Models Reference

⚠️ Documentation Under Review: This documentation is currently being updated and verified against the actual implementation. Some information may be incorrect or incomplete. Please verify all code examples against the actual source code before use.

Overview

The CMS provides data models representing core entities: users, posts, pages, events, categories, tags, event categories, and authentication tokens. Models encapsulate business logic, define relationships, and provide data persistence through the Neuron ORM.

All models extend Neuron\Orm\Model and use PHP 8 attributes for ORM configuration.

Model Classes

User Model

Class: Neuron\Cms\Models\User

Table: users

Description: Represents a user account with authentication, authorization, and profile attributes. Supports role-based access control, brute force protection, two-factor authentication, and remember me functionality.

Properties

Property Type Default Description
$_id ?int null Primary key (auto-increment)
$_username string - Unique username (3-50 characters)
$_email string - Unique email address
$_passwordHash string - Hashed password (Argon2id/Bcrypt)
$_role string subscriber User role (admin, editor, author, subscriber)
$_status string active Account status (active, inactive, suspended)
$_emailVerified bool false Email verification status
$_rememberToken ?string null Remember me token (hashed SHA-256)
$_twoFactorSecret ?string null TOTP secret for 2FA
$_twoFactorRecoveryCodes ?array null Recovery codes for 2FA
$_failedLoginAttempts int 0 Failed login counter for brute force protection
$_lockedUntil ?DateTimeImmutable null Account lockout expiration timestamp
$_createdAt ?DateTimeImmutable Current time User creation timestamp
$_updatedAt ?DateTimeImmutable null Last update timestamp
$_lastLoginAt ?DateTimeImmutable null Last successful login timestamp
$_timezone string UTC User's timezone for date/time display

Relationships

Relationship Type Target Foreign Key Description
$_posts HasMany Post author_id Posts authored by this user

Enums

UserRole Enum (Neuron\Cms\Enums\UserRole):

Case Value Description
UserRole::Admin admin Administrator with full system access
UserRole::Editor editor Editor with content management access
UserRole::Author author Author with own content management access
UserRole::Subscriber subscriber Subscriber with read-only access (default)

UserStatus Enum (Neuron\Cms\Enums\UserStatus):

Case Value Description
UserStatus::Active active Active account, can log in
UserStatus::Inactive inactive Inactive account (pending email verification)
UserStatus::Suspended suspended Suspended account, cannot log in

Note: The deprecated constants (ROLE_, STATUS_) still exist in the User model for backward compatibility but should not be used in new code.

Methods

Identity Methods:

getId(): ?int
setId( int $id ): self

getUsername(): string
setUsername( string $username ): self

getEmail(): string
setEmail( string $email ): self

Authentication Methods:

getPasswordHash(): string
setPasswordHash( string $passwordHash ): self

getRememberToken(): ?string
setRememberToken( ?string $rememberToken ): self

getTwoFactorSecret(): ?string
setTwoFactorSecret( ?string $twoFactorSecret ): self
hasTwoFactorEnabled(): bool  // Returns true if 2FA is enabled

getTwoFactorRecoveryCodes(): ?array
setTwoFactorRecoveryCodes( ?array $twoFactorRecoveryCodes ): self

Role Methods:

getRole(): string
setRole( string $role ): self

isAdmin(): bool       // Check if role is admin
isEditor(): bool      // Check if role is editor
isAuthor(): bool      // Check if role is author

Status Methods:

getStatus(): string
setStatus( string $status ): self

isActive(): bool      // Check if status is active
isSuspended(): bool   // Check if status is suspended

Email Verification Methods:

isEmailVerified(): bool
setEmailVerified( bool $emailVerified ): self

Brute Force Protection Methods:

getFailedLoginAttempts(): int
setFailedLoginAttempts( int $failedLoginAttempts ): self
incrementFailedLoginAttempts(): self
resetFailedLoginAttempts(): self  // Resets attempts and clears lockout

getLockedUntil(): ?DateTimeImmutable
setLockedUntil( ?DateTimeImmutable $lockedUntil ): self
isLockedOut(): bool  // Returns true if currently locked

Timestamp Methods:

getCreatedAt(): ?DateTimeImmutable
setCreatedAt( DateTimeImmutable $createdAt ): self

getUpdatedAt(): ?DateTimeImmutable
setUpdatedAt( ?DateTimeImmutable $updatedAt ): self

getLastLoginAt(): ?DateTimeImmutable
setLastLoginAt( ?DateTimeImmutable $lastLoginAt ): self

Utility Methods:

getTimezone(): string
setTimezone( string $timezone ): self

toArray(): array  // Convert user to array (for database)
static fromArray( array $data ): static  // Create user from array

Usage Examples

Creating a new user:

use Neuron\Cms\Models\User;

$user = new User();
$user->setUsername( 'john_doe' );
$user->setEmail( '[email protected]' );
$user->setPasswordHash( password_hash( 'SecurePass123', PASSWORD_ARGON2ID ) );
$user->setRole( User::ROLE_AUTHOR );
$user->setStatus( User::STATUS_ACTIVE );
$user->setEmailVerified( true );

Checking user roles:

if( $user->isAdmin() )
{
    // Grant admin access
}

if( $user->getRole() === User::ROLE_EDITOR )
{
    // Grant editor access
}

Handling failed logins:

// Increment failed attempts
$user->incrementFailedLoginAttempts();

// Check if lockout threshold reached (5 attempts)
if( $user->getFailedLoginAttempts() >= 5 )
{
    $lockoutDuration = 15; // minutes
    $lockedUntil = (new DateTimeImmutable())->modify( "+{$lockoutDuration} minutes" );
    $user->setLockedUntil( $lockedUntil );
}

// Check if user is locked
if( $user->isLockedOut() )
{
    throw new Exception( 'Account is temporarily locked' );
}

// Reset on successful login
$user->resetFailedLoginAttempts();
$user->setLastLoginAt( new DateTimeImmutable() );

Enable two-factor authentication:

// Generate and store TOTP secret
$secret = $twoFactorAuthenticator->generateSecret();
$user->setTwoFactorSecret( $secret );

// Generate recovery codes
$recoveryCodes = $twoFactorAuthenticator->generateRecoveryCodes();
$user->setTwoFactorRecoveryCodes( $recoveryCodes );

// Check if 2FA is enabled
if( $user->hasTwoFactorEnabled() )
{
    // Require 2FA code
}

Post Model

Class: Neuron\Cms\Models\Post

Table: posts

Description: Represents a blog post with Editor.js content, metadata, and relationships to categories, tags, and author.

Properties

Property Type Default Description
$_id ?int null Primary key (auto-increment)
$_title string - Post title
$_slug string - URL-friendly slug (unique)
$_body string '' Plain text content (derived from Editor.js JSON)
$_contentRaw string {"blocks":[]} Editor.js JSON content string
$_excerpt ?string null Post excerpt/summary
$_featuredImage ?string null Featured image URL/path
$_authorId int - Author user ID (foreign key)
$_status string draft Publication status (draft, published, scheduled)
$_publishedAt ?DateTimeImmutable null Publication timestamp
$_viewCount int 0 View counter for analytics
$_createdAt ?DateTimeImmutable Current time Creation timestamp
$_updatedAt ?DateTimeImmutable null Last update timestamp

Relationships

Relationship Type Target Pivot Table Description
$_author BelongsTo User - Post author
$_categories BelongsToMany Category post_categories Post categories (many-to-many)
$_tags BelongsToMany Tag post_tags Post tags (many-to-many)

Constants

Constant Value Description
STATUS_DRAFT draft Draft status (not publicly visible)
STATUS_PUBLISHED published Published status (publicly visible)
STATUS_SCHEDULED scheduled Scheduled for future publication

Methods

Identity & Content Methods:

getId(): ?int
setId( int $id ): self

getTitle(): string
setTitle( string $title ): self

getSlug(): string
setSlug( string $slug ): self

getBody(): string  // Plain text content
setBody( string $body ): self

getContent(): array  // Decoded Editor.js JSON as array
getContentRaw(): string  // Raw Editor.js JSON string
setContent( string $jsonContent ): self  // Set from JSON string
setContentArray( array $content ): self  // Set from array (encodes to JSON)

getExcerpt(): ?string
setExcerpt( ?string $excerpt ): self

getFeaturedImage(): ?string
setFeaturedImage( ?string $featuredImage ): self

Author Methods:

getAuthorId(): int
setAuthorId( int $authorId ): self

getAuthor(): ?User
setAuthor( ?User $author ): self  // Also sets author_id

Status & Publishing Methods:

getStatus(): string
setStatus( string $status ): self

isPublished(): bool
isDraft(): bool
isScheduled(): bool

getPublishedAt(): ?DateTimeImmutable
setPublishedAt( ?DateTimeImmutable $publishedAt ): self

View Tracking Methods:

getViewCount(): int
setViewCount( int $viewCount ): self
incrementViewCount(): self  // Increments view count by 1

Category Methods:

getCategories(): array  // Returns Category[]
setCategories( array $categories ): self  // Set all categories

addCategory( Category $category ): self  // Add single category
removeCategory( Category $category ): self  // Remove single category
hasCategory( Category $category ): bool  // Check if post has category

Tag Methods:

getTags(): array  // Returns Tag[]
setTags( array $tags ): self  // Set all tags

addTag( Tag $tag ): self  // Add single tag
removeTag( Tag $tag ): self  // Remove single tag
hasTag( Tag $tag ): bool  // Check if post has tag

Timestamp Methods:

getCreatedAt(): ?DateTimeImmutable
setCreatedAt( DateTimeImmutable $createdAt ): self

getUpdatedAt(): ?DateTimeImmutable
setUpdatedAt( ?DateTimeImmutable $updatedAt ): self

Utility Methods:

toArray(): array  // Convert post to array
static fromArray( array $data ): static  // Create post from array

Usage Examples

Creating a new post:

use Neuron\Cms\Models\Post;

$post = new Post();
$post->setTitle( 'Getting Started with Neuron CMS' );
$post->setSlug( 'getting-started-with-neuron-cms' );

// Set content using Editor.js JSON
$editorContent = [
    'blocks' => [
        [
            'type' => 'header',
            'data' => [
                'text' => 'Welcome to Neuron CMS',
                'level' => 2
            ]
        ],
        [
            'type' => 'paragraph',
            'data' => [
                'text' => 'This is a comprehensive guide...'
            ]
        ]
    ]
];
$post->setContentArray( $editorContent );

$post->setExcerpt( 'Learn the basics of Neuron CMS...' );
$post->setAuthorId( $user->getId() );
$post->setStatus( ContentStatus::Published );
$post->setPublishedAt( new DateTimeImmutable() );

Working with categories and tags:

// Add categories
$post->addCategory( $wordpressCategory );
$post->addCategory( $phpCategory );

// Add tags
$post->addTag( $tutorialTag );
$post->addTag( $beginnerTag );

// Check relationships
if( $post->hasCategory( $phpCategory ) )
{
    // Post is in PHP category
}

// Get all categories
$categories = $post->getCategories();
foreach( $categories as $category )
{
    echo $category->getName();
}

Publishing workflow:

// Save as draft
$post->setStatus( ContentStatus::Draft );

// Publish immediately
$post->setStatus( ContentStatus::Published );
$post->setPublishedAt( new DateTimeImmutable() );

// Schedule for future publication
$publishDate = new DateTimeImmutable( '2025-12-01 09:00:00' );
$post->setStatus( ContentStatus::Scheduled );
$post->setPublishedAt( $publishDate );

// Check status
if( $post->isPublished() )
{
    // Display on site
}

Tracking views:

// Increment view count when post is viewed
$post->incrementViewCount();
$postRepository->update( $post );

// Get view statistics
$popularPosts = $postRepository->findByViewCount( $limit = 10 );

Category Model

Class: Neuron\Cms\Models\Category

Table: categories

Description: Represents a blog post category for organizing content.

Properties

Property Type Default Description
$_id ?int null Primary key (auto-increment)
$_name string - Category name
$_slug string - URL-friendly slug (unique)
$_description ?string null Category description
$_createdAt ?DateTimeImmutable Current time Creation timestamp
$_updatedAt ?DateTimeImmutable null Last update timestamp

Relationships

Relationship Type Target Pivot Table Description
$_posts BelongsToMany Post post_categories Posts in this category

Methods

getId(): ?int
setId( int $id ): self

getName(): string
setName( string $name ): self

getSlug(): string
setSlug( string $slug ): self

getDescription(): ?string
setDescription( ?string $description ): self

getCreatedAt(): ?DateTimeImmutable
setCreatedAt( DateTimeImmutable $createdAt ): self

getUpdatedAt(): ?DateTimeImmutable
setUpdatedAt( ?DateTimeImmutable $updatedAt ): self

toArray(): array
static fromArray( array $data ): static

Usage Examples

use Neuron\Cms\Models\Category;

$category = new Category();
$category->setName( 'PHP Development' );
$category->setSlug( 'php-development' );
$category->setDescription( 'Articles about PHP programming and frameworks' );

Tag Model

Class: Neuron\Cms\Models\Tag

Table: tags

Description: Represents a blog post tag for content classification and filtering.

Properties

Property Type Default Description
$_id ?int null Primary key (auto-increment)
$_name string - Tag name
$_slug string - URL-friendly slug (unique)
$_createdAt ?DateTimeImmutable Current time Creation timestamp
$_updatedAt ?DateTimeImmutable null Last update timestamp

Relationships

Relationship Type Target Pivot Table Description
$_posts BelongsToMany Post post_tags Posts with this tag

Methods

getId(): ?int
setId( int $id ): self

getName(): string
setName( string $name ): self

getSlug(): string
setSlug( string $slug ): self

getCreatedAt(): ?DateTimeImmutable
setCreatedAt( DateTimeImmutable $createdAt ): self

getUpdatedAt(): ?DateTimeImmutable
setUpdatedAt( ?DateTimeImmutable $updatedAt ): self

toArray(): array
static fromArray( array $data ): static

Usage Examples

use Neuron\Cms\Models\Tag;

$tag = new Tag();
$tag->setName( 'Tutorial' );
$tag->setSlug( 'tutorial' );

Page Model

Class: Neuron\Cms\Models\Page

Table: pages

Description: Represents a CMS page with Editor.js content, templates, and SEO metadata.

Properties

Property Type Default Description
$_id ?int null Primary key (auto-increment)
$_title string - Page title
$_slug string - URL-friendly slug (unique)
$_contentRaw string {"blocks":[]} Editor.js JSON content string
$_template string default Page template name
$_metaTitle ?string null SEO meta title
$_metaDescription ?string null SEO meta description
$_metaKeywords ?string null SEO meta keywords
$_authorId int - Author user ID (foreign key)
$_status string draft Publication status (draft, published)
$_publishedAt ?DateTimeImmutable null Publication timestamp
$_viewCount int 0 View counter
$_createdAt ?DateTimeImmutable Current time Creation timestamp
$_updatedAt ?DateTimeImmutable null Last update timestamp

Relationships

Relationship Type Target Description
$_author BelongsTo User Page author

Constants

Status Constants:

Constant Value Description
STATUS_DRAFT draft Draft status (not publicly visible)
STATUS_PUBLISHED published Published status (publicly visible)

Template Constants:

Constant Value Description
TEMPLATE_DEFAULT default Default page template
TEMPLATE_FULL_WIDTH full-width Full-width page template (no sidebar)
TEMPLATE_SIDEBAR sidebar Page template with sidebar
TEMPLATE_LANDING landing Landing page template

Methods

getId(): ?int
setId( int $id ): self

getTitle(): string
setTitle( string $title ): self

getSlug(): string
setSlug( string $slug ): self

getContent(): array  // Decoded Editor.js JSON as array
getContentRaw(): string  // Raw Editor.js JSON string
setContent( string $jsonContent ): self
setContentArray( array $content ): self

getTemplate(): string
setTemplate( string $template ): self

getMetaTitle(): ?string
setMetaTitle( ?string $metaTitle ): self

getMetaDescription(): ?string
setMetaDescription( ?string $metaDescription ): self

getMetaKeywords(): ?string
setMetaKeywords( ?string $metaKeywords ): self

getAuthorId(): int
setAuthorId( int $authorId ): self

getAuthor(): ?User
setAuthor( ?User $author ): self

getStatus(): string
setStatus( string $status ): self

isPublished(): bool
isDraft(): bool

getPublishedAt(): ?DateTimeImmutable
setPublishedAt( ?DateTimeImmutable $publishedAt ): self

getViewCount(): int
setViewCount( int $viewCount ): self
incrementViewCount(): self

getCreatedAt(): ?DateTimeImmutable
setCreatedAt( DateTimeImmutable $createdAt ): self

getUpdatedAt(): ?DateTimeImmutable
setUpdatedAt( ?DateTimeImmutable $updatedAt ): self

toArray(): array
static fromArray( array $data ): static

Usage Examples

use Neuron\Cms\Models\Page;

$page = new Page();
$page->setTitle( 'About Us' );
$page->setSlug( 'about-us' );
$page->setTemplate( Page::TEMPLATE_FULL_WIDTH );

// Set SEO metadata
$page->setMetaTitle( 'About Our Company - Neuron CMS' );
$page->setMetaDescription( 'Learn about our mission, vision, and values...' );
$page->setMetaKeywords( 'company, about, mission, values' );

// Set content
$content = [
    'blocks' => [
        [
            'type' => 'header',
            'data' => ['text' => 'Our Story', 'level' => 1]
        ],
        [
            'type' => 'paragraph',
            'data' => ['text' => 'Founded in 2020...']
        ]
    ]
];
$page->setContentArray( $content );

$page->setStatus( Page::STATUS_PUBLISHED );
$page->setAuthorId( $user->getId() );

Event Model

Class: Neuron\Cms\Models\Event

Table: events

Description: Represents a calendar event with date/time, location, category, and Editor.js content. Events can be all-day or timed, single or multi-day, and support rich content with images and contact information.

Properties

Property Type Default Description
$_id ?int null Primary key (auto-increment)
$_title string - Event title
$_slug string - URL-friendly slug (unique)
$_description ?string null Short event description/summary
$_contentRaw string {"blocks":[]} Editor.js JSON content string
$_location ?string null Event location (venue, address, etc.)
$_startDate DateTimeImmutable - Event start date and time
$_endDate ?DateTimeImmutable null Event end date and time (optional for single-day events)
$_allDay bool false Whether event is all-day (no specific time)
$_categoryId ?int null Event category ID (foreign key)
$_status string draft Publication status (draft, published)
$_featuredImage ?string null Featured image URL/path
$_organizer ?string null Event organizer name
$_contactEmail ?string null Contact email for event inquiries
$_contactPhone ?string null Contact phone for event inquiries
$_createdBy ?int null Creator user ID (foreign key)
$_viewCount int 0 View counter
$_createdAt ?DateTimeImmutable Current time Creation timestamp
$_updatedAt ?DateTimeImmutable null Last update timestamp

Relationships

Relationship Type Target Description
$_creator BelongsTo User Event creator
$_category BelongsTo EventCategory Event category

Constants

Status Constants:

Constant Value Description
STATUS_DRAFT draft Draft status (not publicly visible)
STATUS_PUBLISHED published Published status (publicly visible)

Methods

getId(): ?int
setId( int $id ): self

getTitle(): string
setTitle( string $title ): self

getSlug(): string
setSlug( string $slug ): self

getDescription(): ?string
setDescription( ?string $description ): self

getContent(): array  // Decoded Editor.js JSON as array
getContentRaw(): string  // Raw Editor.js JSON string
setContent( string $jsonContent ): self
setContentArray( array $content ): self

getLocation(): ?string
setLocation( ?string $location ): self

getStartDate(): DateTimeImmutable
setStartDate( DateTimeImmutable $startDate ): self

getEndDate(): ?DateTimeImmutable
setEndDate( ?DateTimeImmutable $endDate ): self

isAllDay(): bool
setAllDay( bool $allDay ): self

getCategoryId(): ?int
setCategoryId( ?int $categoryId ): self

getCategory(): ?EventCategory
setCategory( ?EventCategory $category ): self

getStatus(): string
setStatus( string $status ): self

isPublished(): bool
isDraft(): bool

getFeaturedImage(): ?string
setFeaturedImage( ?string $featuredImage ): self

getOrganizer(): ?string
setOrganizer( ?string $organizer ): self

getContactEmail(): ?string
setContactEmail( ?string $contactEmail ): self

getContactPhone(): ?string
setContactPhone( ?string $contactPhone ): self

getCreatedBy(): ?int
setCreatedBy( int $createdBy ): self

getCreator(): ?User
setCreator( ?User $creator ): self

getViewCount(): int
setViewCount( int $viewCount ): self
incrementViewCount(): self

getCreatedAt(): ?DateTimeImmutable
setCreatedAt( DateTimeImmutable $createdAt ): self

getUpdatedAt(): ?DateTimeImmutable
setUpdatedAt( ?DateTimeImmutable $updatedAt ): self

// Event timing helper methods
isUpcoming(): bool  // Event starts in the future
isPast(): bool      // Event ended in the past
isOngoing(): bool   // Event is currently happening

toArray(): array
static fromArray( array $data ): static

Usage Examples

use Neuron\Cms\Models\Event;
use Neuron\Cms\Models\EventCategory;

// Create a single-day event
$event = new Event();
$event->setTitle( 'PHP Conference 2025' );
$event->setSlug( 'php-conference-2025' );
$event->setDescription( 'Annual PHP developers conference' );
$event->setLocation( 'Convention Center, Downtown' );
$event->setStartDate( new DateTimeImmutable( '2025-06-15 09:00:00' ) );
$event->setEndDate( new DateTimeImmutable( '2025-06-15 17:00:00' ) );
$event->setStatus( Event::STATUS_PUBLISHED );

// Create an all-day event
$event = new Event();
$event->setTitle( 'Company Retreat' );
$event->setStartDate( new DateTimeImmutable( '2025-07-01' ) );
$event->setEndDate( new DateTimeImmutable( '2025-07-03' ) );
$event->setAllDay( true );

// Set event content with Editor.js
$content = [
    'blocks' => [
        [
            'type' => 'header',
            'data' => ['text' => 'Event Details', 'level' => 2]
        ],
        [
            'type' => 'paragraph',
            'data' => ['text' => 'Join us for an exciting day of learning...']
        ]
    ]
];
$event->setContentArray( $content );

// Set organizer and contact info
$event->setOrganizer( 'PHP User Group' );
$event->setContactEmail( '[email protected]' );
$event->setContactPhone( '555-0123' );

// Assign category
$event->setCategory( $conferenceCategory );

// Check event timing
if( $event->isUpcoming() ) {
    echo "Event is coming soon!";
}

if( $event->isOngoing() ) {
    echo "Event is happening now!";
}

EventCategory Model

Class: Neuron\Cms\Models\EventCategory

Table: event_categories

Description: Represents a category for organizing events (e.g., "Conferences", "Workshops", "Meetups"). Categories can have colors for visual distinction and are used to filter and organize calendar events.

Properties

Property Type Default Description
$_id ?int null Primary key (auto-increment)
$_name string - Category name
$_slug string - URL-friendly slug (unique)
$_color string #3b82f6 Hex color code for visual distinction
$_description ?string null Category description
$_createdAt ?DateTimeImmutable Current time Creation timestamp
$_updatedAt ?DateTimeImmutable null Last update timestamp

Relationships

Relationship Type Target Description
$_events BelongsToMany Event Events in this category

Methods

getId(): ?int
setId( int $id ): self

getName(): string
setName( string $name ): self

getSlug(): string
setSlug( string $slug ): self

getColor(): string
setColor( string $color ): self

getDescription(): ?string
setDescription( ?string $description ): self

getCreatedAt(): ?DateTimeImmutable
setCreatedAt( DateTimeImmutable $createdAt ): self

getUpdatedAt(): ?DateTimeImmutable
setUpdatedAt( ?DateTimeImmutable $updatedAt ): self

getEvents(): array  // Event[]
setEvents( array $events ): self

toArray(): array
static fromArray( array $data ): static

Usage Examples

use Neuron\Cms\Models\EventCategory;

// Create a category
$category = new EventCategory();
$category->setName( 'Conferences' );
$category->setSlug( 'conferences' );
$category->setColor( '#ff6b6b' );  // Red color
$category->setDescription( 'Tech conferences and industry events' );

// Create categories with different colors
$workshop = new EventCategory();
$workshop->setName( 'Workshops' )
         ->setSlug( 'workshops' )
         ->setColor( '#4ecdc4' );  // Teal

$meetup = new EventCategory();
$meetup->setName( 'Meetups' )
       ->setSlug( 'meetups' )
       ->setColor( '#ffd93d' );  // Yellow

// Use in calendar views for color-coding events
foreach( $events as $event )
{
    $category = $event->getCategory();
    echo "<div style='border-left: 4px solid {$category->getColor()}'>";
    echo $event->getTitle();
    echo "</div>";
}

PasswordResetToken Model

Class: Neuron\Cms\Models\PasswordResetToken

Table: password_reset_tokens

Description: Represents a one-time password reset token with expiration.

Properties

Property Type Default Description
$_id ?int null Primary key (auto-increment)
$_email string - User email address
$_token string - Hashed token (SHA-256)
$_createdAt DateTimeImmutable Current time Token creation timestamp
$_expiresAt DateTimeImmutable Created + 60 min Token expiration timestamp

Methods

__construct( string $email = '', string $token = '', int $expirationMinutes = 60 )

getId(): ?int
setId( int $id ): self

getEmail(): string
setEmail( string $email ): self

getToken(): string  // Returns hashed token
setToken( string $token ): self

getCreatedAt(): DateTimeImmutable
setCreatedAt( DateTimeImmutable $createdAt ): self

getExpiresAt(): DateTimeImmutable
setExpiresAt( DateTimeImmutable $expiresAt ): self

isExpired(): bool  // Returns true if token has expired

toArray(): array
static fromArray( array $data ): self

Usage Examples

use Neuron\Cms\Models\PasswordResetToken;

// Generate token
$plainToken = bin2hex( random_bytes( 32 ) );  // 64-character hex string
$hashedToken = hash( 'sha256', $plainToken );

// Create token entity
$token = new PasswordResetToken( email: '[email protected]',
    token: $hashedToken,
    expirationMinutes: 60 );

// Check if expired
if( $token->isExpired() )
{
    throw new Exception( 'Password reset token has expired' );
}

EmailVerificationToken Model

Class: Neuron\Cms\Models\EmailVerificationToken

Table: email_verification_tokens

Description: Represents a one-time email verification token with expiration.

Properties

Property Type Default Description
$_id ?int null Primary key (auto-increment)
$_userId int - User ID (foreign key)
$_token string - Hashed token (SHA-256)
$_createdAt DateTimeImmutable Current time Token creation timestamp
$_expiresAt DateTimeImmutable Created + 60 min Token expiration timestamp

Methods

__construct( int $userId = 0, string $token = '', int $expirationMinutes = 60 )

getId(): ?int
setId( int $id ): self

getUserId(): int
setUserId( int $userId ): self

getToken(): string  // Returns hashed token
setToken( string $token ): self

getCreatedAt(): DateTimeImmutable
setCreatedAt( DateTimeImmutable $createdAt ): self

getExpiresAt(): DateTimeImmutable
setExpiresAt( DateTimeImmutable $expiresAt ): self

isExpired(): bool  // Returns true if token has expired

toArray(): array
static fromArray( array $data ): self

Usage Examples

use Neuron\Cms\Models\EmailVerificationToken;

// Generate token
$plainToken = bin2hex( random_bytes( 32 ) );  // 64-character hex string
$hashedToken = hash( 'sha256', $plainToken );

// Create token entity
$token = new EmailVerificationToken( userId: $user->getId(),
    token: $hashedToken,
    expirationMinutes: 60
);

// Check if expired
if( $token->isExpired() )
{
    throw new Exception( 'Email verification token has expired' );
}

Model Relationships

One-to-Many Relationships

User has many Posts:

class User extends Model
{
    #[HasMany( Post::class, foreignKey: 'author_id' )]
    private array $_posts = [];
}

Usage:

// Get all posts by user
$posts = $userRepository->findPostsByUserId( $user->getId() );

User has many Pages:

class Page extends Model
{
    #[BelongsTo( User::class, foreignKey: 'author_id' )]
    private ?User $_author = null;
}

Usage:

// Get page author
$author = $page->getAuthor();

Many-to-Many Relationships

Post belongs to many Categories:

class Post extends Model
{
    #[BelongsToMany( Category::class, pivotTable: 'post_categories' )]
    private array $_categories = [];
}

Usage:

// Add category to post
$post->addCategory( $category );

// Get all categories for post
$categories = $post->getCategories();

Post belongs to many Tags:

class Post extends Model
{
    #[BelongsToMany( Tag::class, pivotTable: 'post_tags' )]
    private array $_tags = [];
}

Usage:

// Add tag to post
$post->addTag( $tag );

// Get all tags for post
$tags = $post->getTags();

Pivot Tables

Many-to-many relationships require pivot tables:

post_categories table:

Column Type Description
post_id int Foreign key to posts table
category_id int Foreign key to categories table

post_tags table:

Column Type Description
post_id int Foreign key to posts table
tag_id int Foreign key to tags table

Model Validation

Built-in Validation

Models do not perform validation directly. Use DTOs (Data Transfer Objects) and repositories for validation:

Example with DTO:

use Neuron\Dto\Dto;

class PostDto extends Dto
{
    protected function getRules(): array
    {
        return [
            'title' => ['required', 'string', 'min:3', 'max:255'],
            'slug' => ['required', 'string', 'regex:/^[a-z0-9-]+$/'],
            'body' => ['required', 'string'],
            'author_id' => ['required', 'integer', 'min:1'],
            'status' => ['required', 'in:draft,published,scheduled']
        ];
    }
}

Usage:

try
{
    $dto = PostDto::fromArray( $request->post() );

    // Create post from validated DTO
    $post = Post::fromArray( $dto->toArray() );
    $postRepository->create( $post );
}
catch( ValidationException $e )
{
    // Handle validation errors
    $errors = $e->getErrors();
}

Custom Validation Rules

Create custom validation in services:

class PostService
{
    public function validatePost( Post $post ): void
    {
        // Custom slug validation
        if( !$this->isSlugUnique( $post->getSlug(), $post->getId() ) )
        {
            throw new ValidationException( 'Slug must be unique' );
        }

        // Custom author validation
        $author = $this->userRepository->findById( $post->getAuthorId() );
        if( !$author || !$author->isActive() )
        {
            throw new ValidationException( 'Invalid author' );
        }

        // Custom publication date validation
        if( $post->getStatus() === ContentStatus::Scheduled )
        {
            if( !$post->getPublishedAt() )
            {
                throw new ValidationException( 'Scheduled posts must have publication date' );
            }

            if( $post->getPublishedAt() < new DateTimeImmutable() )
            {
                throw new ValidationException( 'Publication date must be in the future' );
            }
        }
    }

    private function isSlugUnique( string $slug, ?int $excludeId = null ): bool
    {
        $existing = $this->postRepository->findBySlug( $slug );

        if( !$existing )
{
            return true;
        }

        // Allow same slug if it's the same post being updated
        if( $excludeId && $existing->getId() === $excludeId )
        {
            return true;
        }

        return false;
    }
}

Extending Models

Adding Custom Properties

Extend models to add application-specific properties:

namespace App\Models;

use Neuron\Cms\Models\User as BaseUser;
use Neuron\Orm\Attributes\Table;

#[Table( 'users' )]
class User extends BaseUser
{
    private ?string $_phoneNumber = null;
    private ?string $_company = null;
    private ?string $_jobTitle = null;

    public function getPhoneNumber(): ?string
    {
        return $this->_phoneNumber;
    }

    public function setPhoneNumber( ?string $phoneNumber ): self
    {
        $this->_phoneNumber = $phoneNumber;
        return $this;
    }

    public function getCompany(): ?string
    {
        return $this->_company;
    }

    public function setCompany( ?string $company ): self
    {
        $this->_company = $company;
        return $this;
    }

    public function getJobTitle(): ?string
    {
        return $this->_jobTitle;
    }

    public function setJobTitle( ?string $jobTitle ): self
    {
        $this->_jobTitle = $jobTitle;
        return $this;
    }

    public function toArray(): array
    {
        return array_merge( parent::toArray(),
            [
                'phone_number' => $this->_phoneNumber,
                'company' => $this->_company,
                'job_title' => $this->_jobTitle
            ] );
    }
}

Adding Custom Methods

Add business logic methods to extended models:

namespace App\Models;

use Neuron\Cms\Models\Post as BasePost;

class Post extends BasePost
{
    /**
     * Get estimated reading time in minutes
     */
    public function getReadingTime(): int
    {
        $wordCount = str_word_count( $this->getBody() );
        $wordsPerMinute = 200;
        return (int) ceil( $wordCount / $wordsPerMinute );
    }

    /**
     * Check if post was published within the last 7 days
     */
    public function isNew(): bool
    {
        if( !$this->getPublishedAt() )
        {
            return false;
        }

        $sevenDaysAgo = (new \DateTimeImmutable())->modify( '-7 days' );
        return $this->getPublishedAt() > $sevenDaysAgo;
    }

    /**
     * Get related posts by shared categories
     */
    public function getRelatedPosts( int $limit = 5 ): array
    {
        // Implementation would use repository
        return $this->postRepository->findRelatedByCategories( $this, $limit );
    }

    /**
     * Generate excerpt from body if not set
     */
    public function getExcerpt(): ?string
    {
        $excerpt = parent::getExcerpt();

        if( $excerpt )
{
            return $excerpt;
        }

        // Auto-generate from body (first 200 characters)
        $body = $this->getBody();
        if( strlen( $body ) > 200 )
        {
            return substr( $body, 0, 197 ) . '...';
        }

        return $body;
    }
}

Creating Custom Models

Create completely new models for custom entities:

namespace App\Models;

use DateTimeImmutable;
use Neuron\Orm\Model;
use Neuron\Orm\Attributes\Table;

#[Table( 'products' )]
class Product extends Model
{
    private ?int $_id = null;
    private string $_name;
    private string $_sku;
    private float $_price;
    private int $_stockQuantity = 0;
    private ?DateTimeImmutable $_createdAt = null;
    private ?DateTimeImmutable $_updatedAt = null;

    public function __construct()
    {
        $this->_createdAt = new DateTimeImmutable();
    }

    public function getId(): ?int
    {
        return $this->_id;
    }

    public function setId( int $id ): self
    {
        $this->_id = $id;
        return $this;
    }

    public function getName(): string
    {
        return $this->_name;
    }

    public function setName( string $name ): self
    {
        $this->_name = $name;
        return $this;
    }

    public function getSku(): string
    {
        return $this->_sku;
    }

    public function setSku( string $sku ): self
    {
        $this->_sku = $sku;
        return $this;
    }

    public function getPrice(): float
    {
        return $this->_price;
    }

    public function setPrice( float $price ): self
    {
        $this->_price = $price;
        return $this;
    }

    public function getStockQuantity(): int
    {
        return $this->_stockQuantity;
    }

    public function setStockQuantity( int $stockQuantity ): self
    {
        $this->_stockQuantity = $stockQuantity;
        return $this;
    }

    public function isInStock(): bool
    {
        return $this->_stockQuantity > 0;
    }

    public function toArray(): array
    {
        return [
            'id' => $this->_id,
            'name' => $this->_name,
            'sku' => $this->_sku,
            'price' => $this->_price,
            'stock_quantity' => $this->_stockQuantity,
            'created_at' => $this->_createdAt?->format( 'Y-m-d H:i:s' ),
            'updated_at' => $this->_updatedAt?->format( 'Y-m-d H:i:s' )
        ];
    }

    public static function fromArray( array $data ): static
    {
        $product = new self();

        if( isset( $data['id'] ) )
        {
            $product->setId( (int ) $data['id'] );
        }

        $product->setName( $data['name'] ?? '' );
        $product->setSku( $data['sku'] ?? '' );
        $product->setPrice( (float )($data['price'] ?? 0.0) );
        $product->setStockQuantity( (int )($data['stock_quantity'] ?? 0) );

        if( isset( $data['created_at'] ) && $data['created_at'] )
        {
            $product->_createdAt = new DateTimeImmutable( $data['created_at'] );
        }

        if( isset( $data['updated_at'] ) && $data['updated_at'] )
        {
            $product->_updatedAt = new DateTimeImmutable( $data['updated_at'] );
        }

        return $product;
    }
}

Best Practices

Immutable Timestamps

Use DateTimeImmutable instead of DateTime for timestamps to prevent unintended mutations:

// Good
private ?DateTimeImmutable $_createdAt = null;

// Bad
private ?DateTime $_createdAt = null;

Fluent Setters

Return $this from setter methods to enable method chaining:

$user->setUsername( 'john' )
     ->setEmail( '[email protected]' )
     ->setRole( User::ROLE_AUTHOR );

Type Safety

Use strict types for properties and method signatures:

private string $_username;  // Not ?string if required
private ?string $_excerpt = null;  // Nullable for optional fields

public function setUsername( string $username ): self  // Strict type hint

Validation in Services

Keep validation logic in services, not models:

// Good
class UserService
{
    public function validateUser( User $user ): void
    {
        // Validation logic
    }
}

// Bad
class User extends Model
{
    public function validate(): void
    {
        // Don't put validation in models
    }
}

Constants for Enums

Use constants for enum-like values:

class Post extends Model
{
    public const STATUS_DRAFT = 'draft';
    public const STATUS_PUBLISHED = 'published';
    public const STATUS_SCHEDULED = 'scheduled';
}

// Usage
$post->setStatus( ContentStatus::Published );

Null Safety

Handle null values appropriately:

// Good
public function getExcerpt(): ?string
{
    return $this->_excerpt;
}

// Good - conditional logic
$excerpt = $post->getExcerpt() ?? 'No excerpt available';

// Bad - will throw error if null
$length = strlen( $post->getExcerpt() );

Additional Resources