Core Concepts
Understanding entities, fields, service providers, and the kernel lifecycle
This guide covers the foundational concepts every Waaseyaa developer needs: the entity/field model, service providers, the kernel lifecycle, and the package system.
The Entity/Field Model
Waaseyaa's content model is inspired by Drupal but rebuilt with modern PHP. Every piece of structured content is an entity. Articles, users, taxonomy terms, media. All entities.
Entity Types
An entity type is a definition that describes a class of entities. You define it with the EntityType value object:
use Waaseyaa\Entity\EntityType;
$articleType = new EntityType(
id: 'article',
label: 'Article',
class: App\Entity\Article::class,
keys: [
'id' => 'id',
'uuid' => 'uuid',
'label' => 'title',
'bundle' => 'bundle',
'revision' => 'revision_id',
'langcode' => 'langcode',
],
revisionable: true,
translatable: true,
fieldDefinitions: [
'title' => ['type' => 'string', 'label' => 'Title', 'required' => true],
'body' => ['type' => 'text', 'label' => 'Body'],
],
);
This defines an article entity type with revision tracking, translation support, and two fields.
Key properties:
| Property | Purpose |
|---|---|
id |
Machine name used throughout the system |
class |
PHP class that extends EntityBase |
keys |
Maps logical keys (id, uuid, label) to field names |
revisionable |
Whether this type tracks revision history |
translatable |
Whether this type supports multiple languages |
fieldDefinitions |
Declares the typed fields on this entity |
bundleEntityType |
Optional: the config entity that provides bundles (e.g., node_type for node) |
Content Entities vs Config Entities
Waaseyaa has two kinds of entities:
- Content entities (
ContentEntityBase) store user-created content like articles, comments, and media. They are fieldable, potentially revisionable, and translatable. - Config entities (
ConfigEntityBase) store site configuration like content types, vocabularies, and workflows. They are exported to YAML and synced between environments.
// Content entity: declare machine name + key map on the class (PHP 8 attributes).
use Waaseyaa\Entity\Attribute\ContentEntityKeys;
use Waaseyaa\Entity\Attribute\ContentEntityType;
use Waaseyaa\Entity\ContentEntityBase;
#[ContentEntityType(id: 'article')]
#[ContentEntityKeys(label: 'title')]
class Article extends ContentEntityBase {}
// Config entity: still uses explicit type id + keys on the class today
class ArticleType extends ConfigEntityBase
{
protected string $entityTypeId = 'article_type';
protected array $entityKeys = ['id' => 'id', 'label' => 'label'];
}
Content entities resolve entityTypeId / entityKeys from attributes (with inheritance) when you omit them from new Article([...]). Registered EntityType definitions must agree with that metadata—see the Entity system guide.
Content entities use auto-increment integer IDs. Config entities use string IDs and are exported/imported through the config sync system.
Fields and Typed Data
Every content entity implements FieldableInterface, giving it dynamic, typed fields:
interface FieldableInterface
{
public function hasField(string $name): bool;
public function get(string $name): mixed;
public function set(string $name, mixed $value): static;
public function getFieldDefinitions(): array;
}
This interface is how you read and write field values on any content entity.
The waaseyaa/field package provides the field type system. Built-in field types:
| Field Type | Class | Description |
|---|---|---|
string |
StringItem |
Short text values |
text |
TextItem |
Long text with optional format |
integer |
IntegerItem |
Integer values |
float |
FloatItem |
Floating-point numbers |
boolean |
BooleanItem |
True/false values |
entity_reference |
EntityReferenceItem |
References to other entities |
Field types implement FieldTypeInterface:
interface FieldTypeInterface
{
public static function schema(): array;
public static function defaultSettings(): array;
public static function defaultValue(): mixed;
public static function jsonSchema(): array;
}
The jsonSchema() method is what enables the AI packages to automatically generate JSON Schema definitions from your entity fields.
Entity Lifecycle
You manage entities through storage handlers. Here is the typical lifecycle:
// Get the storage handler for an entity type
$storage = $entityTypeManager->getStorage('article');
// Create a new entity
$article = new Article([
'title' => 'My Article',
'body' => 'Content here...',
]);
$article->enforceIsNew(); // Force INSERT (not UPDATE)
$storage->save($article);
// Load an entity by ID
$article = $storage->load(1);
// Update
$article->set('title', 'Updated Title');
$storage->save($article);
// Load multiple
$articles = $storage->loadMultiple([1, 2, 3]);
Call enforceIsNew() before saving entities with pre-set IDs to force an INSERT rather than an UPDATE.
Service Providers
Service providers are the central mechanism for bootstrapping your application. They register services, entity types, routes, and middleware.
The ServiceProvider Class
Every provider extends Waaseyaa\Foundation\ServiceProvider\ServiceProvider:
use Waaseyaa\Foundation\ServiceProvider\ServiceProvider;
class ArticleServiceProvider extends ServiceProvider
{
public function register(): void
{
// Register bindings in the container
$this->singleton(
ArticleRepository::class,
fn () => new ArticleRepository($this->resolve(EntityTypeManagerInterface::class)),
);
}
public function boot(): void
{
// Runs after all providers are registered.
// Use for setup that depends on other services.
}
public function routes(WaaseyaaRouter $router, ?EntityTypeManager $entityTypeManager = null): void
{
// Register HTTP routes
}
public function middleware(EntityTypeManager $entityTypeManager): array
{
// Return HTTP middleware instances
return [];
}
public function commands(EntityTypeManager $entityTypeManager, PdoDatabase $database): array
{
// Return CLI commands
return [];
}
}
Each method has a specific role in the boot sequence. The register() method binds services. The boot() method runs after all providers are registered.
Lifecycle Methods
| Method | When It Runs | Purpose |
|---|---|---|
register() |
First, for all providers | Bind services into the container |
boot() |
After all providers registered | Cross-provider setup |
routes() |
During HTTP kernel boot | Register route definitions |
middleware() |
During HTTP kernel boot | Register middleware classes |
commands() |
During console kernel boot | Register CLI commands |
Container Bindings
Providers offer singleton(), bind(), tag(), and resolve() for dependency management:
public function register(): void
{
// Singleton: same instance every time
$this->singleton(CacheInterface::class, fn () => new MemoryCache());
// Bind: new instance every time
$this->bind(LoggerInterface::class, fn () => new FileLogger('/tmp/app.log'));
// Tags: group services for bulk retrieval
$this->tag(ArticlePolicy::class, 'access_policy');
}
Singletons are resolved once and reused. Bindings create a fresh instance on every call. Tags group related services so you can retrieve them all at once.
The Kernel Lifecycle
The kernel is the entry point for every request. Waaseyaa provides two kernels:
HttpKernelhandles web requests (public/index.php)ConsoleKernelhandles CLI commands (php vendor/bin/waaseyaa)
Both extend AbstractKernel from the waaseyaa/foundation package.
HTTP Request Lifecycle
1. public/index.php creates HttpKernel
2. Kernel discovers and registers all ServiceProviders
3. PackageManifestCompiler scans for providers, policies, middleware
4. register() called on every provider
5. boot() called on every provider
6. Router matches the request to a route
7. Middleware pipeline executes (authentication, CSRF, etc.)
8. AccessChecker evaluates route access options
9. SSR dispatches `Class::method` app controllers through `AppControllerMethodInvoker`, which builds typed action arguments (services, `Request`, route entities, scalars, `#[MapRoute]` / `#[MapQuery]` bags)
10. Controller method executes and returns `Response` (or an Inertia page object)
11. Response sent to the client
This sequence runs on every HTTP request. Steps 2-5 are cached after the first boot when you run optimize:manifest.
Boot Optimization
The PackageManifestCompiler scans your application for providers, access policies, and middleware using PHP attributes. After adding new providers or policies, rebuild the manifest:
php vendor/bin/waaseyaa optimize:manifest
This compiles discovery results into a cached manifest, avoiding runtime reflection.
The Package System
Waaseyaa's architecture is defined by how packages compose together.
Layer Rules
Each package declares its architectural layer (0-6). A package may only import from packages at its own layer or below. This is enforced by convention and tested in CI:
Layer 0 (Foundation) depends on: nothing
Layer 1 (Core Data) depends on: Layer 0
Layer 2 (Services) depends on: Layers 0-1
...and so on
This strict layering prevents circular dependencies and keeps the architecture clean.
Composability
You install only the packages you need. If you only need entities and routing without AI or SSR, install waaseyaa/core. If you need everything, install waaseyaa/full.
Package Interfaces
Every package exposes its public API through interfaces:
EntityTypeManagerInterface— notEntityTypeManagerConfigFactoryInterface— notConfigFactoryAccessPolicyInterface— not a concrete policy class
You can swap implementations for testing (using in-memory backends) or extend behavior without touching framework internals.
Next Steps
- Entity System — Deep dive into entity types, revisions, and translations
- Routing Guide — The RouteBuilder API, middleware, and access control
- Access Control — The deny-unless-granted permission model