⚠️ 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.
The CMS implements a service layer that encapsulates business logic and orchestrates operations across repositories and models. Services provide a clean API for complex operations, handle validation, emit events, and enforce business rules.
Services are organized by domain:
| Domain | Services | Description |
|---|---|---|
| Auth | Authentication, EmailVerifier, CsrfToken, PasswordResetter | Authentication and security workflows |
| User | Creator, Updater, Deleter | User account management |
| Post | Creator, Updater, Publisher, Deleter | Blog post management |
| Category | Creator, Updater, Deleter | Category management |
| Tag | Creator, Resolver | Tag management and resolution |
| Page | Creator, Updater, Deleter | CMS page management |
| Sender | Email composition and delivery | |
| Member | RegistrationService | Member registration workflow |
| Content | ShortcodeParser, EditorJsRenderer | Content parsing and rendering |
| Widget | IWidget, Widget, WidgetRegistry, WidgetRenderer | Widget system |
| Dto | DtoFactoryService | DTO creation and caching |
Authentication and security-related services.
Namespace: Neuron\Cms\Services\Auth\Authentication
Handles user authentication, session management, brute force protection, and remember me functionality.
public function __construct( IUserRepository $userRepository,
SessionManager $sessionManager,
PasswordHasher $passwordHasher )
| Parameter | Type | Description |
|---|---|---|
$userRepository |
IUserRepository |
User data access |
$sessionManager |
SessionManager |
Session management |
$passwordHasher |
PasswordHasher |
Password hashing and verification |
public function setMaxLoginAttempts( int $maxLoginAttempts ): self
public function setLockoutDuration( int $lockoutDuration ): self
Default Values:
attempt( string $username, string $password, bool $remember = false ): bool
Attempt to authenticate a user with username and password.
Parameters:
$username: Username or email$password: Plain text password$remember: Enable remember me functionality (30 days)Returns: true if authentication successful, false otherwise
Behavior:
UserLoginEvent on success or UserLoginFailedEvent on failureExample:
$auth = new Authentication( $userRepo, $sessionMgr, $hasher );
$auth->setMaxLoginAttempts( 5 )
->setLockoutDuration( 15 );
if( $auth->attempt( 'john', 'password123', true ) )
{
// Authentication successful
$user = $auth->user();
}
else
{
// Authentication failed
}
login( User $user, bool $remember = false ): void
Manually log in a user (bypasses password check).
Parameters:
$user: User to log in$remember: Enable remember me functionalityBehavior:
UserLoginEventlogout(): void
Log out the current user.
Behavior:
UserLogoutEvent with session durationcheck(): bool
Check if a user is authenticated.
Returns: true if user is logged in (session or remember token), false otherwise
user(): ?User
Get the currently authenticated user.
Returns: Current user or null if not authenticated
id(): ?int
Get the current user's ID.
Returns: User ID or null if not authenticated
validateCredentials( User $user, string $password ): bool
Validate password for a user.
Returns: true if password matches, false otherwise
hasRole( string $role ): bool
Check if current user has a specific role.
isAdmin(): bool
Check if current user is an admin.
isEditorOrHigher(): bool
Check if current user is an editor or admin.
isAuthorOrHigher(): bool
Check if current user is an author, editor, or admin.
The Authentication service implements secure remember me functionality:
Namespace: Neuron\Cms\Services\Auth\EmailVerifier
Handles email verification token generation, validation, and account activation.
public function __construct( IEmailVerificationTokenRepository $tokenRepository,
IUserRepository $userRepository,
SettingManager $settings,
string $basePath,
string $verificationUrl )
| Parameter | Type | Description |
|---|---|---|
$tokenRepository |
IEmailVerificationTokenRepository |
Token data access |
$userRepository |
IUserRepository |
User data access |
$settings |
SettingManager |
Email configuration |
$basePath |
string |
Base path for email templates |
$verificationUrl |
string |
Base URL for verification links |
setTokenExpirationMinutes( int $minutes ): self
Set token expiration time (default: 60 minutes).
sendVerificationEmail( User $user ): bool
Send verification email to a user.
Behavior:
true on successThrows: Exception if email sending fails
validateToken( string $plainToken ): ?EmailVerificationToken
Validate a verification token.
Returns: Token object if valid and not expired, null otherwise
verifyEmail( string $plainToken ): bool
Verify email and activate user account.
Behavior:
EmailVerifiedEventReturns: true if verification successful, false if token invalid
resendVerification( string $email ): bool
Resend verification email to a user by email address.
Behavior:
true immediately if email not foundfalse if user already verifiedcleanupExpiredTokens(): int
Clean up expired tokens from database.
Returns: Number of tokens deleted
Example:
$verifier = new EmailVerifier( $tokenRepo,
$userRepo,
$settings,
'/path/to/app',
'https://example.com/verify-email' );
$verifier->setTokenExpirationMinutes( 120 );
// Send verification email
$verifier->sendVerificationEmail( $user );
// Verify email from URL parameter
if( $verifier->verifyEmail( $_GET['token'] ) )
{
// Email verified successfully
}
Namespace: Neuron\Cms\Services\Auth\CsrfToken
Generates and validates CSRF tokens to prevent Cross-Site Request Forgery attacks.
public function __construct( SessionManager $sessionManager )
generate(): string
Generate a new CSRF token (64 characters).
Behavior:
getToken(): string
Get the current CSRF token (generates if doesn't exist).
validate( string $token ): bool
Validate a CSRF token.
Behavior:
hash_equals() for timing-safe comparisontrue if token matches stored tokenregenerate(): string
Regenerate CSRF token (same as generate).
Example:
$csrf = new CsrfToken( $sessionManager );
// In form rendering
$token = $csrf->getToken();
echo "<input type='hidden' name='csrf_token' value='{$token}'>";
// In form processing
if( !$csrf->validate( $_POST['csrf_token'] ) )
{
throw new Exception( 'CSRF validation failed' );
}
Namespace: Neuron\Cms\Services\Auth\PasswordResetter
Handles password reset token generation, validation, and password updates.
public function __construct( IPasswordResetTokenRepository $tokenRepository,
IUserRepository $userRepository,
PasswordHasher $passwordHasher,
SettingManager $settings,
string $basePath,
string $resetUrl )
| Parameter | Type | Description |
|---|---|---|
$tokenRepository |
IPasswordResetTokenRepository |
Token data access |
$userRepository |
IUserRepository |
User data access |
$passwordHasher |
PasswordHasher |
Password hashing |
$settings |
SettingManager |
Email configuration |
$basePath |
string |
Base path for email templates |
$resetUrl |
string |
Base URL for reset links |
setTokenExpirationMinutes( int $minutes ): self
Set token expiration time (default: 60 minutes).
requestReset( string $email ): bool
Request a password reset for an email address.
Behavior:
true even if email not found (security feature)PasswordResetRequestedEventThrows: Exception if email sending fails
validateToken( string $plainToken ): ?PasswordResetToken
Validate a reset token.
Returns: Token object if valid and not expired, null otherwise
resetPassword( string $plainToken, string $newPassword ): bool
Reset password using a valid token.
Behavior:
PasswordResetCompletedEventReturns: true if password reset successful, false if token invalid
Throws: Exception if password doesn't meet requirements
cleanupExpiredTokens(): int
Clean up expired tokens from database.
Returns: Number of tokens deleted
Example:
$resetter = new PasswordResetter( $tokenRepo,
$userRepo,
$hasher,
$settings,
'/path/to/app',
'https://example.com/reset-password' );
// Request reset
$resetter->requestReset( '[email protected]' );
// Reset password from URL parameter
try
{
if( $resetter->resetPassword( $_GET['token'], $_POST['new_password'] ) )
{
// Password reset successfully
}
}
catch( Exception $e )
{
// Password validation failed
echo $e->getMessage();
}
User account management services.
Namespace: Neuron\Cms\Services\User\Creator
Creates new users with password hashing and validation.
public function __construct( IUserRepository $userRepository,
PasswordHasher $passwordHasher )
create( string $username, string $email, string $password, string $role ): User
Create a new user.
Parameters:
$username: Username$email: Email address$password: Plain text password$role: User role (admin, editor, author, subscriber)Returns: Created user with ID
Behavior:
UserCreatedEventThrows: Exception if password doesn't meet requirements or creation fails
Example:
$creator = new Creator( $userRepo, $passwordHasher );
try
{
$user = $creator->create( 'john_doe',
'[email protected]',
'SecurePassword123!',
User::ROLE_AUTHOR );
}
catch( Exception $e )
{
echo "User creation failed: " . $e->getMessage();
}
Namespace: Neuron\Cms\Services\User\Updater
Updates existing users and their passwords.
public function __construct( IUserRepository $userRepository,
PasswordHasher $passwordHasher )
update( User $user, string $username, string $email, string $role, ?string $password = null, ?string $timezone = null ): User
Update an existing user.
Parameters:
$user: User to update$username: New username$email: New email address$role: New role$password: Optional new password (validated and hashed if provided)$timezone: Optional timezoneReturns: Updated user
Behavior:
UserUpdatedEventThrows: Exception if password doesn't meet requirements or update fails
Example:
$updater = new Updater( $userRepo, $passwordHasher );
$updater->update( $user,
'john_doe',
'[email protected]',
User::ROLE_EDITOR,
'NewPassword123!', // Optional: update password
'America/New_York' // Optional: update timezone );
Namespace: Neuron\Cms\Services\User\Deleter
Handles safe deletion of users.
public function __construct( IUserRepository $userRepository )
delete( int $userId ): bool
Delete a user by ID.
Returns: true if deletion successful
Behavior:
UserDeletedEventThrows: Exception if user not found
Example:
$deleter = new Deleter( $userRepo );
try
{
if( $deleter->delete( 123 ) )
{
echo "User deleted successfully";
}
}
catch( Exception $e )
{
echo "Deletion failed: " . $e->getMessage();
}
Blog post management services.
Namespace: Neuron\Cms\Services\Post\Creator
Creates new blog posts with categories and tags relationships.
public function __construct( IPostRepository $postRepository,
ICategoryRepository $categoryRepository,
TagResolver $tagResolver )
create( string $title, string $content, int $authorId, string $status, ?string $slug = null, ?string $excerpt = null, ?string $featuredImage = null, array $categoryIds = [], string $tagNames = '' ): Post
Create a new post.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$title |
string |
Post title |
$content |
string |
Editor.js JSON content |
$authorId |
int |
Author user ID |
$status |
string |
Post status (draft, published, scheduled) |
$slug |
?string |
Optional custom slug (auto-generated from title if not provided) |
$excerpt |
?string |
Optional excerpt |
$featuredImage |
?string |
Optional featured image URL |
$categoryIds |
array |
Array of category IDs |
$tagNames |
string |
Comma-separated tag names |
Returns: Created post with ID and relationships
Behavior:
uniqid()Example:
$creator = new Creator( $postRepo, $categoryRepo, $tagResolver );
$post = $creator->create( 'My First Post',
'{"blocks":[{"type":"paragraph","data":{"text":"Hello World"}}]}',
$user->getId(),
Post::STATUS_PUBLISHED,
'my-first-post',
'This is my first blog post',
'/images/featured.jpg',
[1, 2], // Category IDs
'php, tutorial, beginner' // Tags
);
Namespace: Neuron\Cms\Services\Post\Updater
Updates existing posts and their relationships.
public function __construct( IPostRepository $postRepository,
ICategoryRepository $categoryRepository,
TagResolver $tagResolver )
update( Post $post, string $title, string $content, string $status, ?string $slug = null, ?string $excerpt = null, ?string $featuredImage = null, array $categoryIds = [], string $tagNames = '' ): Post
Update an existing post.
Parameters: Same as Post Creator create() method, except first parameter is the post to update
Returns: Updated post
Behavior:
Example:
$updater = new Updater( $postRepo, $categoryRepo, $tagResolver );
$updater->update( $post,
'Updated Title',
'{"blocks":[...]}',
Post::STATUS_PUBLISHED,
null, // Keep existing slug
'Updated excerpt',
null, // Keep existing featured image
[1, 3], // New category IDs
'php, advanced' // New tags );
Namespace: Neuron\Cms\Services\Post\Publisher
Handles publishing, unpublishing, and scheduling posts.
public function __construct( IPostRepository $postRepository )
publish( Post $post ): Post
Publish a draft post.
Behavior:
Returns: Updated post
Throws: Exception if post already published or persistence fails
unpublish( Post $post ): Post
Unpublish a post (revert to draft).
Behavior:
Returns: Updated post
Throws: Exception if post not published or persistence fails
schedule( Post $post, DateTimeImmutable $publishAt ): Post
Schedule a post for future publication.
Parameters:
$post: Post to schedule$publishAt: When to publishBehavior:
Returns: Updated post
Throws: Exception if date is in the past or persistence fails
Example:
$publisher = new Publisher( $postRepo );
// Publish immediately
$publisher->publish( $post );
// Schedule for future
$futureDate = new DateTimeImmutable( '+7 days' );
$publisher->schedule( $post, $futureDate );
// Unpublish
$publisher->unpublish( $post );
Namespace: Neuron\Cms\Services\Post\Deleter
Handles safe deletion of posts.
public function __construct( IPostRepository $postRepository )
delete( Post $post ): bool
Delete a post.
Returns: true if deletion successful
deleteById( int $id ): bool
Delete a post by ID.
Returns: true if deletion successful
Example:
$deleter = new Deleter( $postRepo );
// Delete by object
$deleter->delete( $post );
// Delete by ID
$deleter->deleteById( 123 );
Category management services.
Namespace: Neuron\Cms\Services\Category\Creator
Creates new categories with slug generation.
public function __construct( ICategoryRepository $categoryRepository )
create( string $name, string $slug, ?string $description = null ): Category
Create a new category.
Parameters:
$name: Category name$slug: URL-friendly slug (auto-generated from name if empty)$description: Optional descriptionReturns: Created category with ID
Behavior:
uniqid()CategoryCreatedEventExample:
$creator = new Creator( $categoryRepo );
$category = $creator->create( 'Web Development',
'web-development',
'Articles about web development' );
Namespace: Neuron\Cms\Services\Category\Updater
Updates existing categories.
public function __construct( ICategoryRepository $categoryRepository )
update( Category $category, string $name, string $slug, ?string $description = null ): Category
Update an existing category.
Parameters: Same as Category Creator, except first parameter is the category to update
Returns: Updated category
Behavior:
CategoryUpdatedEventExample:
$updater = new Updater( $categoryRepo );
$updater->update( $category,
'Frontend Development',
'frontend-development',
'Updated description' );
Namespace: Neuron\Cms\Services\Category\Deleter
Handles safe deletion of categories.
public function __construct( ICategoryRepository $categoryRepository )
delete( int $categoryId ): bool
Delete a category by ID.
Returns: true if deletion successful
Behavior:
CategoryDeletedEventThrows: Exception if category not found
Example:
$deleter = new Deleter( $categoryRepo );
try
{
$deleter->delete( 5 );
}
catch( Exception $e )
{
echo "Deletion failed: " . $e->getMessage();
}
Tag management and resolution services.
Namespace: Neuron\Cms\Services\Tag\Creator
Creates individual tags with slug generation.
public function __construct( ITagRepository $tagRepository )
create( string $name, ?string $slug = null ): Tag
Create a new tag.
Parameters:
$name: Tag name$slug: Optional custom slug (auto-generated from name if not provided)Returns: Created tag with ID
Behavior:
uniqid()Example:
$creator = new Creator( $tagRepo );
$tag = $creator->create( 'PHP 8', 'php-8' );
Namespace: Neuron\Cms\Services\Tag\Resolver
Resolves comma-separated tag names to Tag objects, creating tags that don't exist.
public function __construct( ITagRepository $tagRepository,
Creator $tagCreator )
resolveFromString( string $tagNames ): array
Resolve comma-separated tag names to Tag objects.
Parameters:
$tagNames: Comma-separated tag names (e.g., "php, tutorial, beginner")Returns: Array of Tag objects
Behavior:
Example:
$resolver = new Resolver( $tagRepo, $tagCreator );
$tags = $resolver->resolveFromString( 'php, tutorial, beginner' );
// Returns: [Tag( 'php' ), Tag( 'tutorial' ), Tag( 'beginner' )]
// Creates any tags that didn't already exist
$post->setTags( $tags );
CMS page management services.
Namespace: Neuron\Cms\Services\Page\Creator
Creates new pages with validation and slug generation.
public function __construct( IPageRepository $pageRepository )
create( string $title, string $content, int $authorId, string $status, ?string $slug = null, string $template = Page::TEMPLATE_DEFAULT, ?string $metaTitle = null, ?string $metaDescription = null, ?string $metaKeywords = null ): Page
Create a new page.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$title |
string |
Page title |
$content |
string |
Editor.js JSON content |
$authorId |
int |
Author user ID |
$status |
string |
Page status (draft, published) |
$slug |
?string |
Optional custom slug (auto-generated if not provided) |
$template |
string |
Template name (default, full-width, sidebar, landing) |
$metaTitle |
?string |
SEO meta title |
$metaDescription |
?string |
SEO meta description |
$metaKeywords |
?string |
SEO meta keywords |
Returns: Created page with ID
Behavior:
uniqid()Example:
$creator = new Creator( $pageRepo );
$page = $creator->create( 'About Us',
'{"blocks":[...]}',
$user->getId(),
Page::STATUS_PUBLISHED,
'about',
Page::TEMPLATE_DEFAULT,
'About Our Company',
'Learn more about our company and mission',
'company, about, mission'
);
Namespace: Neuron\Cms\Services\Page\Updater
Updates existing pages with validation.
public function __construct( IPageRepository $pageRepository )
update( Page $page, string $title, string $content, string $status, ?string $slug = null, string $template = Page::TEMPLATE_DEFAULT, ?string $metaTitle = null, ?string $metaDescription = null, ?string $metaKeywords = null ): bool
Update an existing page.
Parameters: Same as Page Creator, except first parameter is the page to update
Returns: true if update successful
Behavior:
Example:
$updater = new Updater( $pageRepo );
$updater->update( $page,
'About Our Company',
'{"blocks":[...]}',
Page::STATUS_PUBLISHED,
'about',
Page::TEMPLATE_FULL_WIDTH,
'Updated Meta Title',
'Updated meta description',
'updated, keywords' );
Namespace: Neuron\Cms\Services\Page\Deleter
Handles page deletion.
public function __construct( IPageRepository $pageRepository )
delete( Page $page ): bool
Delete a page.
Returns: true if deletion successful
Example:
$deleter = new Deleter( $pageRepo );
$deleter->delete( $page );
Event creation, modification, and deletion for calendar/event management.
Namespace: Neuron\Cms\Services\Event\Creator
Creates new calendar events with validation.
public function __construct( IEventRepository $eventRepository,
IEventCategoryRepository $categoryRepository,
?IRandom $random = null )
| Parameter | Type | Description |
|---|---|---|
$eventRepository |
IEventRepository |
Event data access |
$categoryRepository |
IEventCategoryRepository |
Category validation |
$random |
?IRandom |
Random ID generator for slug fallback |
create( ... ): Event
Create a new event.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
$title |
string |
Yes | - | Event title |
$startDate |
DateTimeImmutable |
Yes | - | Event start date/time |
$createdBy |
int |
Yes | - | Creator user ID |
$status |
string |
Yes | - | Status (draft, published) |
$slug |
?string |
No | null |
Custom slug (auto-generated if null) |
$description |
?string |
No | null |
Short description |
$contentRaw |
string |
No | {"blocks":[]} |
Editor.js JSON content |
$location |
?string |
No | null |
Event location |
$endDate |
?DateTimeImmutable |
No | null |
End date/time |
$allDay |
bool |
No | false |
All-day event flag |
$categoryId |
?int |
No | null |
Event category ID |
$featuredImage |
?string |
No | null |
Featured image URL |
$organizer |
?string |
No | null |
Organizer name |
$contactEmail |
?string |
No | null |
Contact email |
$contactPhone |
?string |
No | null |
Contact phone |
Returns: Created Event with ID
Throws: RuntimeException if slug exists or category not found
Example:
use Neuron\Cms\Services\Event\Creator;
use Neuron\Cms\Models\Event;
$creator = new Creator( $eventRepo, $categoryRepo );
// Create simple event
$event = $creator->create( title: 'PHP Conference 2025',
startDate: new DateTimeImmutable( '2025-06-15 09:00:00' ),
createdBy: $userId,
status: Event::STATUS_PUBLISHED,
location: 'Convention Center',
description: 'Annual PHP developers conference'
);
// Create all-day multi-day event
$event = $creator->create( title: 'Company Retreat',
startDate: new DateTimeImmutable( '2025-07-01' ),
createdBy: $userId,
status: Event::STATUS_PUBLISHED,
endDate: new DateTimeImmutable( '2025-07-03' ),
allDay: true,
categoryId: $categoryId
);
// Create event with full details
$content = json_encode( [
'blocks' => [
['type' => 'header', 'data' => ['text' => 'Event Details', 'level' => 2]],
['type' => 'paragraph', 'data' => ['text' => 'Join us for...']]
]
] );
$event = $creator->create( title: 'Workshop: Modern PHP',
startDate: new DateTimeImmutable( '2025-08-10 14:00:00' ),
createdBy: $userId,
status: Event::STATUS_DRAFT,
slug: 'modern-php-workshop',
description: 'Learn PHP 8.4+ features',
contentRaw: $content,
location: 'Tech Hub, Room 203',
categoryId: $workshopCategoryId,
organizer: 'PHP User Group',
contactEmail: '[email protected]',
contactPhone: '555-0123'
);
Namespace: Neuron\Cms\Services\Event\Updater
Updates existing events with validation.
public function __construct( IEventRepository $eventRepository,
IEventCategoryRepository $categoryRepository )
update( Event $event, ... ): Event
Update an event.
Parameters: Same as Creator (except no $createdBy)
Returns: Updated Event
Throws: RuntimeException if slug exists for another event or category not found
Example:
$updater = new Updater( $eventRepo, $categoryRepo );
$event = $eventRepo->findById( $eventId );
$updatedEvent = $updater->update( event: $event,
title: 'PHP Conference 2025 (Updated )',
startDate: new DateTimeImmutable( '2025-06-16 09:00:00' ),
status: Event::STATUS_PUBLISHED,
location: 'New Venue, Downtown'
);
Namespace: Neuron\Cms\Services\Event\Deleter
Handles event deletion.
public function __construct( IEventRepository $repository )
delete( Event $event ): bool
Delete an event.
Returns: true if deletion successful
Example:
$deleter = new Deleter( $eventRepo );
$deleter->delete( $event );
Event category creation, modification, and deletion.
Namespace: Neuron\Cms\Services\EventCategory\Creator
Creates new event categories with validation.
public function __construct( IEventCategoryRepository $repository )
create( string $name, ?string $slug = null, string $color = '#3b82f6', ?string $description = null ): EventCategory
Create a new event category.
Parameters:
$name - Category name$slug - Custom slug (auto-generated if null)$color - Hex color code (default: blue #3b82f6)$description - Optional descriptionReturns: Created EventCategory with ID
Throws: RuntimeException if slug already exists
Example:
use Neuron\Cms\Services\EventCategory\Creator;
$creator = new Creator( $categoryRepo );
// Create with defaults
$category = $creator->create( 'Conferences' );
// Create with custom color
$category = $creator->create( name: 'Workshops',
slug: 'workshops',
color: '#ff6b6b',
description: 'Hands-on technical workshops' );
// Create multiple categories with different colors
$conferences = $creator->create( 'Conferences', color: '#3b82f6' );
$workshops = $creator->create( 'Workshops', color: '#4ecdc4' );
$meetups = $creator->create( 'Meetups', color: '#ffd93d' );
Namespace: Neuron\Cms\Services\EventCategory\Updater
Updates existing event categories.
public function __construct( IEventCategoryRepository $repository )
update( EventCategory $category, string $name, string $slug, string $color, ?string $description = null ): EventCategory
Update an event category.
Returns: Updated EventCategory
Throws: RuntimeException if slug exists for another category
Example:
$updater = new Updater( $categoryRepo );
$category = $categoryRepo->findById( $categoryId );
$updatedCategory = $updater->update( category: $category,
name: 'Tech Conferences',
slug: 'tech-conferences',
color: '#0066cc',
description: 'Technology industry conferences' );
Namespace: Neuron\Cms\Services\EventCategory\Deleter
Handles event category deletion.
public function __construct( IEventCategoryRepository $repository )
delete( EventCategory $category ): bool
Delete an event category.
Returns: true if deletion successful
Example:
$deleter = new Deleter( $categoryRepo );
$deleter->delete( $category );
Email composition and delivery.
Namespace: Neuron\Cms\Services\Email\Sender
Sends emails with templates using PHPMailer.
public function __construct( ?SettingManager $settings = null,
string $basePath = '' )
| Parameter | Type | Description |
|---|---|---|
$settings |
?SettingManager |
Email configuration (SMTP, credentials) |
$basePath |
string |
Base path for template loading |
Email settings in config/neuron.yaml:
email:
driver: smtp # smtp, sendmail, or mail
host: smtp.example.com
port: 587
username: [email protected]
password: secret
encryption: tls # tls or ssl
from_address: [email protected]
from_name: Neuron CMS
test_mode: false # If true, logs instead of sending
Recipient Methods (Fluent Interface):
public function to( string $email, string $name = '' ): self
public function cc( string $email, string $name = '' ): self
public function bcc( string $email, string $name = '' ): self
Content Methods (Fluent Interface):
public function subject( string $subject ): self
public function body( string $body, bool $isHtml = true ): self
public function replyTo( string $email, string $name = '' ): self
public function attach( string $filePath, string $name = '' ): self
Template Rendering:
template( string $templatePath, array $data = [] ): self
Render an email template.
Parameters:
$templatePath: Path to template relative to resources/views/ (without .php extension)$data: Data to extract into template scopeBehavior:
Template Location: {basePath}/resources/views/{templatePath}.php
Sending:
send(): bool
Send the email.
Returns: true if email sent successfully, false otherwise
Behavior:
Example:
$sender = new Sender( $settings, '/path/to/app' );
// Simple email
$sender->to( '[email protected]', 'John Doe' )
->subject( 'Welcome!' )
->body( '<h1>Welcome to our site!</h1>' )
->send();
// Email with template
$sender->to( '[email protected]' )
->subject( 'Password Reset Request' )
->template( 'emails/password-reset', [
'ResetLink' => 'https://example.com/reset?token=abc123',
'ExpirationMinutes' => 60
] )
->send();
// Email with attachments
$sender->to( '[email protected]' )
->subject( 'Your Invoice' )
->body( 'Please find your invoice attached.' )
->attach( '/path/to/invoice.pdf', 'invoice.pdf' )
->send();
Test Mode:
When email.test_mode is true, emails are logged instead of sent:
[INFO] TEST MODE - Email not sent
[INFO] To: [email protected]
[INFO] Subject: Password Reset Request
[INFO] Body: Please click the following link to reset your password...
Member registration workflow.
Namespace: Neuron\Cms\Services\Member\RegistrationService
Handles user registration, validation, and email verification.
public function __construct( IUserRepository $userRepository,
PasswordHasher $passwordHasher,
EmailVerifier $emailVerifier,
SettingManager $settings,
?Emitter $emitter = null )
Registration settings in config/neuron.yaml:
member:
registration_enabled: true
require_email_verification: true
default_role: subscriber
isRegistrationEnabled(): bool
Check if registration is enabled.
Returns: true if registration enabled, false otherwise
register( string $username, string $email, string $password, string $passwordConfirmation ): User
Register a new user.
Parameters:
$username: Username (3-50 characters, alphanumeric + underscore)$email: Email address$password: Password (must meet requirements)$passwordConfirmation: Password confirmation (must match)Returns: Created user
Behavior:
UserCreatedEventThrows: Exception if validation fails or registration disabled
registerWithDto( Dto $dto ): User
Register a new user using a RegisterUser DTO.
DTO Fields:
username: Usernameemail: Email addresspassword: Passwordpassword_confirmation: Password confirmationBehavior: Same as register(), but extracts data from DTO
Validation Rules:
| Field | Rules |
|---|---|
| Username | Required, 3-50 characters, alphanumeric + underscore, unique |
| Required, valid email format, unique | |
| Password | Required, meets password requirements |
| Password Confirmation | Required, matches password |
Password Requirements (from PasswordHasher):
Example:
$registration = new RegistrationService( $userRepo,
$passwordHasher,
$emailVerifier,
$settings,
$emitter );
// Check if registration is enabled
if( !$registration->isRegistrationEnabled() )
{
throw new Exception( 'Registration is currently disabled' );
}
// Register with direct parameters
try
{
$user = $registration->register( 'john_doe',
'[email protected]',
'SecurePassword123!',
'SecurePassword123!' );
if( !$user->isEmailVerified() )
{
echo "Please check your email to verify your account.";
}
}
catch( Exception $e )
{
echo "Registration failed: " . $e->getMessage();
}
// Register with DTO
$dto = $dtoFactory->createRegisterUser();
$dto->username = $_POST['username'];
$dto->email = $_POST['email'];
$dto->password = $_POST['password'];
$dto->password_confirmation = $_POST['password_confirmation'];
try
{
$user = $registration->registerWithDto( $dto );
}
catch( Exception $e )
{
echo "Registration failed: " . $e->getMessage();
}
Content parsing and rendering.
Namespace: Neuron\Cms\Services\Content\ShortcodeParser
Parses and renders WordPress-style shortcodes in content.
[shortcode attr="value" attr2="value2"]
[latest-posts limit="5"]
[calendar id="main" max="10"]
public function __construct( ?WidgetRenderer $widgetRenderer = null )
register( string $shortcode, callable $handler ): void
Register a custom shortcode handler.
Parameters:
$shortcode: Shortcode name$handler: Callable that receives attributes array and returns HTMLunregister( string $shortcode ): void
Unregister a shortcode.
hasShortcode( string $shortcode ): bool
Check if shortcode is registered.
parse( string $content ): string
Parse shortcodes in content.
Returns: Content with shortcodes replaced by rendered HTML
Behavior:
[shortcode attr="value"] patternBuilt-in Shortcodes:
[latest-posts]: Render latest posts widgetExample:
$parser = new ShortcodeParser( $widgetRenderer );
// Register custom shortcode
$parser->register( 'greeting', function( $attrs ) {
$name = $attrs['name'] ?? 'Guest';
return "<p>Hello, {$name}!</p>";
});
// Parse content
$content = "Welcome! [greeting name=\"John\"] [latest-posts limit=\"5\"]";
$rendered = $parser->parse( $content );
Custom Shortcode Example:
// Register property listings shortcode
$parser->register( 'property-listings', function( $attrs ) use ( $propertyRepo ) {
$type = $attrs['type'] ?? 'all';
$limit = $attrs['limit'] ?? 10;
$properties = $propertyRepo->findByType( $type, $limit );
$html = '<div class="property-listings">';
foreach( $properties as $property )
{
$html .= '<div class="property-card">...</div>';
}
$html .= '</div>';
return $html;
});
// Use in content
$content = "Available properties: [property-listings type=\"apartment\" limit=\"5\"]";
$rendered = $parser->parse( $content );
Namespace: Neuron\Cms\Services\Content\EditorJsRenderer
Renders Editor.js JSON content to HTML with Bootstrap classes.
public function __construct( ?ShortcodeParser $shortcodeParser = null )
render( array $editorData ): string
Render Editor.js JSON data to HTML.
Parameters:
$editorData: Decoded Editor.js JSON data (array with 'blocks' key)Returns: Rendered HTML with Bootstrap classes
Supported Block Types:
| Block Type | HTML Output | Classes |
|---|---|---|
header |
<h1> to <h6> |
my-4 |
paragraph |
<p> |
mb-3 |
list |
<ul> or <ol> |
mb-3 |
image |
<figure><img> |
img-fluid, border, bg-light, etc. |
quote |
<blockquote> |
blockquote, my-4, alignment classes |
code |
<pre><code> |
my-4 |
delimiter |
<hr> |
my-4 |
raw |
Raw HTML (sanitized) | None |
Behavior:
<b>, <strong>, <i>, <em>, <u>, <a>, <code>, <mark>Example:
$renderer = new EditorJsRenderer( $shortcodeParser );
$editorData = [
'blocks' => [
[
'type' => 'header',
'data' => [
'text' => 'My Article Title',
'level' => 1
]
],
[
'type' => 'paragraph',
'data' => [
'text' => 'This is the first paragraph with <strong>bold</strong> text.'
]
],
[
'type' => 'list',
'data' => [
'style' => 'unordered',
'items' => ['Item 1', 'Item 2', 'Item 3']
]
]
]
];
$html = $renderer->render( $editorData );
Output:
<h1 class='my-4'>My Article Title</h1>
<p class='mb-3'>This is the first paragraph with <strong>bold</strong> text.</p>
<ul class='mb-3'>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
Image Block Example:
[
'type' => 'image',
'data' => [
'file' => ['url' => '/images/photo.jpg'],
'caption' => 'Photo caption',
'stretched' => false,
'withBorder' => true,
'withBackground' => false
]
]
Output:
<figure class='my-4'>
<img src='/images/photo.jpg' class='img-fluid border' alt='Photo caption'>
<figcaption class='text-center text-muted mt-2'>Photo caption</figcaption>
</figure>
Extensible widget/shortcode system.
Namespace: Neuron\Cms\Services\Widget\IWidget
Widget interface for creating custom widgets that can be used as shortcodes.
public function getName(): string
public function render( array $attrs ): string
public function getDescription(): string
public function getAttributes(): array
Example Implementation:
class LatestPostsWidget implements IWidget
{
private IPostRepository $_postRepo;
public function __construct( IPostRepository $postRepo )
{
$this->_postRepo = $postRepo;
}
public function getName(): string
{
return 'latest-posts';
}
public function render( array $attrs ): string
{
$limit = $attrs['limit'] ?? 5;
$posts = $this->_postRepo->getPublished( $limit );
$html = '<div class="latest-posts">';
foreach( $posts as $post )
{
$html .= '<h3>' . htmlspecialchars( $post->getTitle() ) . '</h3>';
}
$html .= '</div>';
return $html;
}
public function getDescription(): string
{
return 'Display latest published posts';
}
public function getAttributes(): array
{
return [
'limit' => 'Number of posts to display (default: 5)'
];
}
}
Namespace: Neuron\Cms\Services\Widget\Widget
Abstract base class for widgets with helper methods.
getDescription(): string
Get widget description (override if needed).
getAttributes(): array
Get supported attributes (override if needed).
attr( array $attrs, string $key, mixed $default = null ): mixed
Helper to get attribute with default value.
view( string $template, array $data = [] ): string
Helper to render a view template.
sanitizeHtml( string $html ): string
Sanitize HTML to prevent XSS.
Example:
class PricingTableWidget extends Widget
{
public function getName(): string
{
return 'pricing-table';
}
public function render( array $attrs ): string
{
$plan = $this->attr( $attrs, 'plan', 'basic' );
$currency = $this->attr( $attrs, 'currency', '$' );
return $this->view( '/path/to/pricing-table.php', [
'plan' => $plan,
'currency' => $currency
] );
}
public function getDescription(): string
{
return 'Display a pricing table';
}
public function getAttributes(): array
{
return [
'plan' => 'Pricing plan to display (default: basic)',
'currency' => 'Currency symbol (default: $)'
];
}
}
Namespace: Neuron\Cms\Services\Widget\WidgetRegistry
Registry for managing widgets with automatic shortcode parser integration.
public function __construct( ShortcodeParser $parser )
register( IWidget $widget ): void
Register a widget.
Behavior:
unregister( string $name ): void
Unregister a widget.
getAll(): array
Get all registered widgets (for documentation).
Returns: Array of IWidget objects
get( string $name ): ?IWidget
Get widget by name.
has( string $name ): bool
Check if widget exists.
Example:
$parser = new ShortcodeParser( $widgetRenderer );
$registry = new WidgetRegistry( $parser );
// Register widgets
$registry->register( new LatestPostsWidget( $postRepo ) );
$registry->register( new CalendarWidget( $eventRepo, $categoryRepo ) );
$registry->register( new PropertyListingsWidget( $propertyRepo ) );
// Use in content
$content = "Check out our [latest-posts limit=\"5\"] and [calendar limit=\"10\" upcoming=\"true\"]";
$rendered = $parser->parse( $content );
// List all registered widgets for documentation
foreach( $registry->getAll() as $widget ) {
echo $widget->getName() . ': ' . $widget->getDescription() . "\n";
foreach( $widget->getAttributes() as $attr => $description ) {
echo " - {$attr}: {$description}\n";
}
}
Namespace: Neuron\Cms\Services\Widget\WidgetRenderer
Renders built-in CMS widgets.
public function __construct( ?IPostRepository $postRepository = null )
render( string $widgetType, array $config ): string
Render a widget by type.
Built-in Widgets:
latest-posts
Render latest published posts.
Attributes:
category: Filter by category slug (optional)limit: Number of posts to show (default: 5)Example:
$renderer = new WidgetRenderer( $postRepo );
// Render latest posts
$html = $renderer->render( 'latest-posts', ['limit' => 5] );
DTO creation and caching.
Namespace: Neuron\Cms\Services\Dto\DtoFactoryService
Centralized service for creating and caching DTOs from YAML definitions.
public function __construct( ?string $dtoDirectory = null )
Default DTO Directory: {cms}/src/Cms/Dtos
create( string $name ): Dto
Get a DTO instance by name.
Parameters:
$name: DTO name (e.g., 'RegisterUser', 'CreatePost')Returns: Fresh DTO instance (cloned from cache)
Behavior:
{dtoDirectory}/{name}Dto.yamlNeuron\Dto\FactoryThrows: Exception if DTO file doesn't exist
Convenience Methods:
public function createRegisterUser(): Dto
public function createCreateUser(): Dto
public function createUpdateUser(): Dto
public function createCreateCategory(): Dto
public function createUpdateCategory(): Dto
public function createCreatePost(): Dto
public function createUpdatePost(): Dto
Cache Management:
public function clearCache(): void
public function getDtoDirectory(): string
Example:
$dtoFactory = new DtoFactoryService();
// Create DTO
$dto = $dtoFactory->createRegisterUser();
$dto->username = 'john_doe';
$dto->email = '[email protected]';
$dto->password = 'SecurePassword123!';
$dto->password_confirmation = 'SecurePassword123!';
// Validate DTO
if( !$dto->validate() ) {
print_r( $dto->getValidationErrors() );
}
// Use with service
$user = $registrationService->registerWithDto( $dto );
Available DTOs:
| DTO Name | File | Description |
|---|---|---|
| RegisterUser | RegisterUserDto.yaml | Member registration |
| CreateUser | CreateUserDto.yaml | Admin user creation |
| UpdateUser | UpdateUserDto.yaml | User updates |
| CreateCategory | CreateCategoryDto.yaml | Category creation |
| UpdateCategory | UpdateCategoryDto.yaml | Category updates |
| CreatePost | CreatePostDto.yaml | Post creation |
| UpdatePost | UpdatePostDto.yaml | Post updates |
Services use constructor injection for dependencies:
class MyController
{
private Authentication $_auth;
private IUserRepository $_userRepo;
public function __construct( Authentication $auth,
IUserRepository $userRepo )
{
$this->_auth = $auth;
$this->_userRepo = $userRepo;
}
public function dashboard()
{
if( !$this->_auth->check() ) {
// Redirect to login
}
$user = $this->_auth->user();
// ...
}
}
// Create dependencies
$userRepo = new UserRepository( $db );
$sessionMgr = new SessionManager();
$passwordHasher = new PasswordHasher();
// Create service
$auth = new Authentication( $userRepo, $sessionMgr, $passwordHasher );
// Use service
if( $auth->attempt( $_POST['username'], $_POST['password'], true ) ) {
// Login successful
}
Services can be composed together:
// Base services
$userRepo = new UserRepository( $db );
$passwordHasher = new PasswordHasher();
$sessionMgr = new SessionManager();
$settings = new SettingManager( $source );
$emitter = new Emitter();
// Authentication services
$auth = new Authentication( $userRepo, $sessionMgr, $passwordHasher );
$emailVerifier = new EmailVerifier( $tokenRepo,
$userRepo,
$settings,
$basePath,
$verificationUrl );
// Registration service (composes multiple services)
$registration = new RegistrationService( $userRepo,
$passwordHasher,
$emailVerifier,
$settings,
$emitter );
// Use composed service
$user = $registration->register( $_POST['username'],
$_POST['email'],
$_POST['password'],
$_POST['password_confirmation'] );
use PHPUnit\Framework\TestCase;
class UserCreatorTest extends TestCase
{
public function testCreateUser()
{
// Create mocks
$userRepo = $this->createMock( IUserRepository::class );
$passwordHasher = $this->createMock( PasswordHasher::class );
// Configure mocks
$passwordHasher->method( 'meetsRequirements' )
->willReturn( true );
$passwordHasher->method( 'hash' )
->willReturn( 'hashed_password' );
$userRepo->method( 'create' )
->willReturnCallback( function( $user ) {
$user->setId( 1 );
return $user;
});
// Create service
$creator = new Creator( $userRepo, $passwordHasher );
// Test
$user = $creator->create( 'john_doe',
'[email protected]',
'Password123!',
User::ROLE_AUTHOR );
$this->assertEquals( 1, $user->getId() );
$this->assertEquals( 'john_doe', $user->getUsername() );
$this->assertEquals( 'hashed_password', $user->getPasswordHash() );
}
}
class RegistrationServiceIntegrationTest extends TestCase
{
private RegistrationService $registration;
private IUserRepository $userRepo;
protected function setUp(): void
{
// Create real database connection
$db = new Database( /* test database config */ );
$this->userRepo = new UserRepository( $db );
// Create real services
$passwordHasher = new PasswordHasher();
$emailVerifier = new EmailVerifier( /* ... */ );
$settings = new SettingManager( /* ... */ );
$this->registration = new RegistrationService( $this->userRepo,
$passwordHasher,
$emailVerifier,
$settings );
// Clean database
$db->exec( 'TRUNCATE TABLE users' );
}
public function testCompleteRegistrationWorkflow()
{
// Register user
$user = $this->registration->register( 'john_doe',
'[email protected]',
'Password123!',
'Password123!' );
// Verify user in database
$this->assertNotNull( $user->getId() );
$dbUser = $this->userRepo->findById( $user->getId() );
$this->assertEquals( 'john_doe', $dbUser->getUsername() );
$this->assertEquals( User::ROLE_SUBSCRIBER, $dbUser->getRole() );
}
}
try
{
$user = $userCreator->create( $username, $email, $password, $role );
}
catch( Exception $e )
{
Log::error( "User creation failed: " . $e->getMessage() );
// Handle error appropriately
}
Validate input before calling service methods:
// Validate DTO before passing to service
$dto = $dtoFactory->createRegisterUser();
$dto->username = $_POST['username'];
$dto->email = $_POST['email'];
$dto->password = $_POST['password'];
$dto->password_confirmation = $_POST['password_confirmation'];
if( !$dto->validate() )
{
$errors = $dto->getValidationErrors();
// Display errors to user
return;
}
// DTO is valid, proceed with registration
$user = $registration->registerWithDto( $dto );
Services should log important operations:
Log::info( "User created: {$user->getUsername()}" );
Log::warning( "Failed login attempt for: {$username}" );
Log::error( "Email sending failed: " . $e->getMessage() );
hash_equals() for token comparisonclass ServiceFactory
{
private array $services = [];
public function getAuthentication(): Authentication
{
if( !isset( $this->services['auth'] ) )
{
$this->services['auth'] = new Authentication( $this->getUserRepository(),
$this->getSessionManager(),
$this->getPasswordHasher() );
}
return $this->services['auth'];
}
// ... other service getters
}
// Register services
Registry::getInstance()->set( 'Authentication', $auth );
Registry::getInstance()->set( 'UserCreator', $userCreator );
// Retrieve services
$auth = Registry::getInstance()->get( 'Authentication' );
$userCreator = Registry::getInstance()->get( 'UserCreator' );
class AuthenticationFilter implements IFilter
{
private Authentication $_auth;
public function __construct( Authentication $auth )
{
$this->_auth = $auth;
}
public function execute( Request $request ): bool
{
if( !$this->_auth->check() )
{
// Redirect to login
header( 'Location: /login' );
return false;
}
return true;
}
}