Common Tasks

⚠️ 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

This cookbook provides step-by-step instructions for frequently performed tasks in the Neuron CMS. Each task includes both programmatic examples and CLI commands where applicable.

User Management Tasks

Creating a New Administrator

Web Interface Method

  1. Log in as an existing administrator
  2. Navigate to Admin > Users > Add New User
  3. Fill in the user details:
    • Username: admin_jane
    • Email: [email protected]
    • Password: Strong password meeting requirements
    • Role: Select Admin
  4. Click Create User

Programmatic Method

use Neuron\Cms\Services\User\Creator;
use Neuron\Cms\Models\User;
use Neuron\Cms\Enums\UserRole;
use Neuron\Cms\Services\Auth\PasswordHasher;
use Neuron\Cms\Repositories\DatabaseUserRepository;
use Neuron\Data\Settings\SettingManager;

// Create service
$userRepo = new DatabaseUserRepository( $settingManager );
$passwordHasher = new PasswordHasher();
$creator = new Creator( $userRepo, $passwordHasher );

// Create admin user
try
{
    $admin = $creator->create( 'admin_jane',
        '[email protected]',
        'SecurePassword123!',
        UserRole::Admin );

    echo "Administrator created with ID: " . $admin->getId();
}
catch( Exception $e )
{
    echo "Error: " . $e->getMessage();
}

CLI Method (Custom Script)

Create scripts/create-admin.php:

<?php
require __DIR__ . '/../vendor/autoload.php';

$app = new \Neuron\Cms\Application( '1.0.0', $settingsSource );

$creator = $app->getContainer()->get( 'UserCreator' );

$admin = $creator->create( $argv[1] ?? 'admin',
    $argv[2] ?? '[email protected]',
    $argv[3] ?? 'ChangeMe123!',
    \Neuron\Cms\Enums\UserRole::Admin );

echo "Admin created: {$admin->getUsername()}\n";

Run:

php scripts/create-admin.php admin_jane [email protected] SecurePassword123!

Resetting a User's Password

Using Password Reset Flow

use Neuron\Cms\Services\Auth\PasswordResetter;

// Initialize service
$resetter = new PasswordResetter( $tokenRepo,
    $userRepo,
    $passwordHasher,
    $settings,
    $basePath,
    'https://example.com/reset-password' );

// Request password reset (sends email)
$resetter->requestReset( '[email protected]' );

Direct Password Change

use Neuron\Cms\Services\User\Updater;

// Find user
$user = $userRepo->findByEmail( '[email protected]' );

if( $user )
{
    // Update with new password
    $updater = new Updater( $userRepo, $passwordHasher );
    $updater->update( $user,
        $user->getUsername(),
        $user->getEmail(),
        $user->getRole(),
        'NewSecurePassword123!' // New password );

    echo "Password updated successfully";
}

CLI Script for Emergency Password Reset

Create scripts/reset-password.php:

<?php
require __DIR__ . '/../vendor/autoload.php';

if( $argc < 3 )
{
    echo "Usage: php reset-password.php <email> <new-password>\n";
    exit( 1 );
}

$app = new \Neuron\Cms\Application( '1.0.0', $settingsSource );

$userRepo = $app->getContainer()->get( 'UserRepository' );
$passwordHasher = $app->getContainer()->get( 'PasswordHasher' );
$updater = new \Neuron\Cms\Services\User\Updater( $userRepo, $passwordHasher );

$user = $userRepo->findByEmail( $argv[1] );

if( !$user )
{
    echo "User not found\n";
    exit( 1 );
}

$updater->update( $user,
    $user->getUsername(),
    $user->getEmail(),
    $user->getRole(),
    $argv[2]
);

echo "Password reset successfully for: {$user->getEmail()}\n";

Run:

php scripts/reset-password.php [email protected] NewPassword123!

Changing User Roles

Promote User to Editor

use Neuron\Cms\Services\User\Updater;
use Neuron\Cms\Enums\UserRole;

$user = $userRepo->findByUsername( 'john_doe' );

$updater = new Updater( $userRepo, $passwordHasher );
$updater->update( $user,
    $user->getUsername(),
    $user->getEmail(),
    UserRole::Editor,  // Promote to editor
    null,              // Don't change password
    $user->getTimezone()
);

echo "User promoted to editor";

Demote Admin to Author

$updater->update( $user,
    $user->getUsername(),
    $user->getEmail(),
    UserRole::Author,  // Demote to author
    null,
    $user->getTimezone()
);

Deactivating User Accounts

Temporary Suspension

use Neuron\Cms\Models\User;

// Find and suspend user
$user = $userRepo->findByUsername( 'spammer123' );
$user->setStatus( User::STATUS_SUSPENDED );
$userRepo->update( $user );

echo "User suspended: {$user->getUsername()}";

Reactivate Account

$user = $userRepo->findByUsername( 'restored_user' );
$user->setStatus( User::STATUS_ACTIVE );
$userRepo->update( $user );

echo "User reactivated: {$user->getUsername()}";

Bulk User Operations

Bulk User Creation from CSV

use Neuron\Cms\Services\User\Creator;
use Neuron\Cms\Models\User;

$creator = new Creator( $userRepo, $passwordHasher );

// Read CSV file
$handle = fopen( 'users.csv', 'r' );
$header = fgetcsv( $handle ); // Skip header

$created = 0;
$errors = [];

while( ($row = fgetcsv( $handle )) !== false )
{
    [$username, $email, $role] = $row;

    try
    {
        // Generate random secure password
        $password = bin2hex( random_bytes( 16 ) );

        $user = $creator->create( $username, $email, $password, $role );
        $created++;

        // Send welcome email with password
        // $emailSender->sendWelcomeEmail( $user, $password );
    }
    catch( Exception $e )
{
        $errors[] = "Failed to create {$username}: " . $e->getMessage();
    }
}

fclose( $handle );

echo "Created {$created} users\n";
if( count( $errors ) > 0 )
{
    echo "Errors:\n" . implode( "\n", $errors );
}

CSV Format (users.csv):

username,email,role
john_doe,[email protected],author
jane_smith,[email protected],editor
bob_jones,[email protected],subscriber

Bulk Role Assignment

use Neuron\Cms\Services\User\Updater;

$updater = new Updater( $userRepo, $passwordHasher );

// Promote all authors to editors
$authors = $userRepo->findByRole( User::ROLE_AUTHOR );

foreach( $authors as $user )
{
    $updater->update( $user,
        $user->getUsername(),
        $user->getEmail(),
        User::ROLE_EDITOR,
        null,
        $user->getTimezone() );

    echo "Promoted: {$user->getUsername()}\n";
}

Content Management Tasks

Creating a Blog Post

Web Interface Workflow

  1. Log in as author or higher role
  2. Navigate to Admin > Posts > Add New Post
  3. Fill in post details:
    • Title: My First Blog Post
    • Slug: my-first-blog-post (auto-generated)
    • Content: Use Editor.js interface to add blocks
    • Excerpt: Brief summary
    • Categories: Select categories
    • Tags: Enter comma-separated tags
    • Featured Image: Upload or enter URL
    • Status: Draft or Published
  4. Click Save Post

Programmatic Creation

use Neuron\Cms\Services\Post\Creator;
use Neuron\Cms\Models\Post;

$creator = new Creator( $postRepo, $categoryRepo, $tagResolver );

// Create Editor.js content
$content = json_encode( [
    'blocks' => [
        [
            'type' => 'header',
            'data' => [
                'text' => 'My First Blog Post',
                'level' => 1
            ]
        ],
        [
            'type' => 'paragraph',
            'data' => [
                'text' => 'This is the introduction paragraph with <strong>bold text</strong>.'
            ]
        ],
        [
            'type' => 'list',
            'data' => [
                'style' => 'unordered',
                'items' => [
                    'First point',
                    'Second point',
                    'Third point'
                ]
            ]
        ]
    ]
] );

$post = $creator->create( 'My First Blog Post',
    $content,
    $currentUser->getId(),
    Post::STATUS_PUBLISHED,
    'my-first-blog-post',
    'This is an introduction to our first blog post.',
    '/images/featured/post-1.jpg',
    [1, 2], // Category IDs
    'php, tutorial, beginners' // Tags
);

echo "Post created: {$post->getSlug()}";

Scheduling Post Publication

use Neuron\Cms\Services\Post\Publisher;

$publisher = new Publisher( $postRepo );

// Schedule post for 7 days from now
$publishDate = new DateTimeImmutable( '+7 days' );

try
{
    $publisher->schedule( $post, $publishDate );
    echo "Post scheduled for: " . $publishDate->format( 'Y-m-d H:i:s' );
}
catch( Exception $e )
{
    echo "Error: " . $e->getMessage();
}

Cron Job for Publishing Scheduled Posts

Create scripts/publish-scheduled.php:

<?php
require __DIR__ . '/../vendor/autoload.php';

$app = new \Neuron\Cms\Application( '1.0.0', $settingsSource );
$postRepo = $app->getContainer()->get( 'PostRepository' );
$publisher = $app->getContainer()->get( 'PostPublisher' );

// Find scheduled posts due for publication
$scheduledPosts = $postRepo->findScheduledDue();

foreach( $scheduledPosts as $post )
{
    try
    {
        // Change status to published
        $post->setStatus( \Neuron\Cms\Models\Post::STATUS_PUBLISHED );
        $postRepo->update( $post );

        echo "Published: {$post->getTitle()}\n";
    }
    catch( Exception $e )
{
        echo "Failed to publish {$post->getTitle()}: " . $e->getMessage() . "\n";
    }
}

Add to crontab:

# Run every hour to publish scheduled posts
0 * * * * cd /path/to/cms && php scripts/publish-scheduled.php >> logs/scheduled-publish.log 2>&1

Organizing Posts with Categories

Creating Category Hierarchy

use Neuron\Cms\Services\Category\Creator;

$creator = new Creator( $categoryRepo );

// Create parent category
$tech = $creator->create( 'Technology',
    'technology',
    'All technology-related posts' );

// Create child categories
$webDev = $creator->create( 'Web Development',
    'web-development',
    'Web development tutorials and tips' );

$mobile = $creator->create( 'Mobile Development',
    'mobile-development',
    'Mobile app development' );

// Set parent relationships (if your model supports hierarchy)
// $webDev->setParentId( $tech->getId() );
// $categoryRepo->update( $webDev );

Assigning Post to Multiple Categories

use Neuron\Cms\Services\Post\Updater;

$updater = new Updater( $postRepo, $categoryRepo, $tagResolver );

// Assign post to multiple categories
$updater->update( $post,
    $post->getTitle(),
    $post->getContentRaw(),
    $post->getStatus(),
    $post->getSlug(),
    $post->getExcerpt(),
    $post->getFeaturedImage(),
    [1, 2, 3], // Category IDs
    'php, tutorial' // Tags
);

Managing Tags

Creating Tags Individually

use Neuron\Cms\Services\Tag\Creator;

$creator = new Creator( $tagRepo );

$phpTag = $creator->create( 'PHP', 'php' );
$tutorialTag = $creator->create( 'Tutorial', 'tutorial' );
$advancedTag = $creator->create( 'Advanced', 'advanced' );

Auto-Creating Tags from String

use Neuron\Cms\Services\Tag\Resolver;

$resolver = new Resolver( $tagRepo, $tagCreator );

// Resolves existing tags and creates new ones
$tags = $resolver->resolveFromString( 'php, tutorial, advanced, new-topic' );

// Assign to post
$post->setTags( $tags );
$postRepo->update( $post );

Bulk Tag Assignment

// Add "featured" tag to all posts by specific author
$posts = $postRepo->findByAuthorId( $authorId );
$featuredTag = $tagRepo->findByName( 'featured' );

foreach( $posts as $post )
{
    $currentTags = $post->getTags();
    if( !$post->hasTag( $featuredTag ) )
    {
        $post->addTag( $featuredTag );
        $postRepo->update( $post );
    }
}

Unpublishing Content

use Neuron\Cms\Services\Post\Publisher;

$publisher = new Publisher( $postRepo );

try
{
    $publisher->unpublish( $post );
    echo "Post unpublished and reverted to draft";
}
catch( Exception $e )
{
    echo "Error: " . $e->getMessage();
}

Deleting Posts

Soft Delete (Recommended)

use Neuron\Cms\Models\Post;

// Mark as inactive/deleted without removing from database
$post->setStatus( 'deleted' );
$postRepo->update( $post );

Hard Delete

use Neuron\Cms\Services\Post\Deleter;

$deleter = new Deleter( $postRepo );

// Permanently delete post
$deleter->delete( $post );

// Or delete by ID
$deleter->deleteById( 123 );

Cascade Deletion (Delete Related Data)

// Before deleting post, handle relationships
$post = $postRepo->findById( $postId );

// Remove all category associations
$post->setCategories( [] );

// Remove all tag associations
$post->setTags( [] );

// Update to persist changes
$postRepo->update( $post );

// Now safe to delete post
$deleter->delete( $post );

Email Tasks

Testing Email Configuration

Send Test Email

use Neuron\Cms\Services\Email\Sender;

$sender = new Sender( $settings, $basePath );

try
{
    $result = $sender
        ->to( '[email protected]', 'Admin' )
        ->subject( 'Test Email - Configuration Check' )
        ->body( '<h1>Test Successful</h1><p>Email configuration is working correctly.</p>' )
        ->send();

    if( $result )
{
        echo "Test email sent successfully!";
    }
else
    {
        echo "Failed to send test email. Check logs.";
    }
}
catch( Exception $e )
{
    echo "Error: " . $e->getMessage();
}

CLI Test Script

Create scripts/test-email.php:

<?php
require __DIR__ . '/../vendor/autoload.php';

$app = new \Neuron\Cms\Application( '1.0.0', $settingsSource );
$settings = $app->getSettingManager();

$sender = new \Neuron\Cms\Services\Email\Sender( $settings, $app->getBasePath() );

$to = $argv[1] ?? '[email protected]';

$result = $sender
    ->to( $to )
    ->subject( 'Test Email' )
    ->body( '<h1>Test</h1><p>This is a test email sent at ' . date('Y-m-d H:i:s' ) . '</p>' )
    ->send();

echo $result ? "Email sent to {$to}\n" : "Failed to send email\n";

Run:

php scripts/test-email.php [email protected]

Customizing Email Templates

Create Custom Template

Create resources/views/emails/custom-welcome.php:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        body {
            font-family: Arial, sans-serif;
            line-height: 1.6;
            color: #333;
        }
        .header {
            background: #4CAF50;
            color: white;
            padding: 20px;
            text-align: center;
        }
        .content {
            padding: 20px;
        }
        .button {
            display: inline-block;
            padding: 10px 20px;
            background: #4CAF50;
            color: white;
            text-decoration: none;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>Welcome to <?= htmlspecialchars( $SiteName ) ?>!</h1>
    </div>
    <div class="content">
        <p>Hello <?= htmlspecialchars( $Username ) ?>,</p>
        <p>Thank you for joining our community. We're excited to have you aboard!</p>
        <p><a href="<?= htmlspecialchars( $ActivationLink ) ?>" class="button">Activate Your Account</a></p>
        <p>If you have any questions, feel free to reply to this email.</p>
        <p>Best regards,<br>The <?= htmlspecialchars( $SiteName ) ?> Team</p>
    </div>
</body>
</html>

Use Custom Template

$sender->to( $user->getEmail(), $user->getUsername() )
       ->subject( "Welcome to {$siteName}" )
       ->template( 'emails/custom-welcome', [
           'SiteName' => $siteName,
           'Username' => $user->getUsername(),
           'ActivationLink' => $activationUrl
       ])
       ->send();

Variable Substitution

Available variables depend on the template context:

Password Reset Template:

Email Verification Template:

Troubleshooting Email Delivery

Enable Test Mode

In config/neuron.yaml:

email:
  test_mode: true  # Logs emails instead of sending

When enabled, check logs:

tail -f logs/application.log | grep "TEST MODE"

SMTP Debugging

Enable SMTP debug output:

use PHPMailer\PHPMailer\PHPMailer;

// In Sender.php or custom implementation
$mail = new PHPMailer( true );
$mail->SMTPDebug = 2; // Enable verbose debug output
$mail->Debugoutput = function( $str, $level ) {
    \Neuron\Log\Log::debug( "SMTP: {$str}" );
};

Check SMTP Credentials

# Test SMTP connection manually
telnet smtp.example.com 587

Or use:

openssl s_client -connect smtp.example.com:465

Common Issues

Issue: Connection timeout

Failed to send email: SMTP connect() failed

Solution: Check firewall rules, SMTP port (587 for TLS, 465 for SSL)

Issue: Authentication failed

SMTP Error: Could not authenticate

Solution: Verify username and password in config/neuron.yaml

Issue: TLS/SSL errors

SMTP Error: TLS negotiation failed

Solution: Check encryption setting (tls vs ssl), server certificate

Maintenance Tasks

Enabling Maintenance Mode

Using Configuration

In config/neuron.yaml:

maintenance:
  enabled: true
  message: "We're performing scheduled maintenance. Back soon!"
  allowed_ips:
    - 127.0.0.1
    - 192.168.1.100

Programmatic Toggle

use Neuron\Patterns\Registry;

// Enable maintenance mode
Registry::getInstance()->set( 'MaintenanceMode', true );

// Check if in maintenance mode
if( Registry::getInstance()->get( 'MaintenanceMode' ) )
{
    http_response_code( 503 );
    echo "Site under maintenance";
    exit;
}

Middleware Filter

The CMS includes a built-in maintenance mode filter that can be applied to routes. See Maintenance Mode Guide for full workflow on enabling/disabling maintenance mode and configuring allowed IPs.

Upgrading CMS

Upgrading the CMS to a newer version involves updating the package via Composer and running the upgrade command.

Standard Upgrade Workflow

# 1. Backup database
mysqldump -u cms_user -p cms_database > backup-$(date +%Y%m%d).sql

# 2. Enable maintenance mode
./vendor/bin/neuron cms:maintenance:enable "Upgrading system"

# 3. Update CMS package
composer update neuron-php/cms

# 4. Run upgrade command
php neuron cms:upgrade

# 5. Apply database migrations
./vendor/bin/neuron db:migrate

# 6. Clear application cache
./vendor/bin/neuron cache:clear

# 7. Test application
# ... verify critical functionality ...

# 8. Disable maintenance mode
./vendor/bin/neuron cms:maintenance:disable

Check for Available Updates

# Preview available updates without applying
php neuron cms:upgrade --check

Output:

Current version: 0.8.5
Latest version: 0.9.0

Update available: 0.8.5 → 0.9.0

New migrations available: 3
Configuration updates: 2

Run 'php neuron cms:upgrade' to apply updates.

Upgrade-Specific Operations

# Copy only new migrations
php neuron cms:upgrade --migrations-only

# Upgrade without updating views (preserves customizations)
php neuron cms:upgrade --skip-views

# Upgrade and run migrations automatically
php neuron cms:upgrade --run-migrations

Automated Upgrade Script

Create scripts/upgrade.sh:

#!/bin/bash
set -e

echo "Starting CMS upgrade..."

# Backup
DATE=$(date +%Y%m%d-%H%M%S)
mysqldump -u cms_user -p cms_database | gzip > "backups/pre-upgrade-$DATE.sql.gz"

# Enable maintenance
./vendor/bin/neuron cms:maintenance:enable "Upgrade in progress"

# Update and upgrade
composer update neuron-php/cms
php neuron cms:upgrade
./vendor/bin/neuron db:migrate
./vendor/bin/neuron cache:clear

# Disable maintenance
./vendor/bin/neuron cms:maintenance:disable

echo "Upgrade completed successfully!"

Make it executable:

chmod +x scripts/upgrade.sh
./scripts/upgrade.sh

Version-Specific Upgrade Notes

Always check version-specific requirements:

# View upgrade notes
cat vendor/neuron-php/cms/UPGRADE_NOTES.md

# Or check online
# https://github.com/neuron-php/cms/blob/main/UPGRADE_NOTES.md

Rollback After Failed Upgrade

If upgrade fails:

# 1. Restore database
gunzip < backups/pre-upgrade-20250112-143000.sql.gz | mysql -u cms_user -p cms_database

# 2. Downgrade package
composer require neuron-php/cms:0.8.5

# 3. Clear cache
./vendor/bin/neuron cache:clear

# 4. Disable maintenance mode
./vendor/bin/neuron cms:maintenance:disable

See Upgrading Guide for comprehensive upgrade procedures and troubleshooting.

Clearing Application Cache

CLI Command

# Clear all cache
./vendor/bin/neuron cache:clear

# Check cache statistics
./vendor/bin/neuron cache:stats

Programmatic Cache Clear

use Neuron\Mvc\Cache\ViewCache;

$cache = Registry::getInstance()->get( 'ViewCache' );

if( $cache instanceof ViewCache )
{
    $cleared = $cache->clear();
    echo "Cleared {$cleared} cache entries";
}

Clear Expired Cache Only

$removed = $app->clearExpiredCache();
echo "Removed {$removed} expired cache entries";

Viewing Application Logs

Log File Locations

logs/
├── application.log     # General application logs
├── error.log          # Error logs
├── access.log         # HTTP access logs
└── scheduled-publish.log  # Scheduled task logs

View Recent Logs

# Last 100 lines
tail -n 100 logs/application.log

# Follow logs in real-time
tail -f logs/application.log

# Filter error logs
grep "ERROR" logs/application.log

# Filter by date
grep "2025-01-28" logs/application.log

Programmatic Log Analysis

// Read last N lines of log
function tail( $file, $lines = 100 )
{
    $handle = fopen( $file, 'r' );
    $linecounter = $lines;
    $pos = -2;
    $beginning = false;
    $text = [];

    while( $linecounter > 0 )
{
        $t = " ";
        while( $t != "\n" )
{
            if( fseek( $handle, $pos, SEEK_END ) == -1 )
            {
                $beginning = true;
                break;
            }
            $t = fgetc( $handle );
            $pos--;
        }
        $linecounter--;
        if( $beginning )
{
            rewind( $handle );
        }
        $text[$lines - $linecounter - 1] = fgets( $handle );
        if( $beginning ) break;
    }
    fclose( $handle );
    return array_reverse( $text );
}

$logLines = tail( 'logs/application.log', 50 );
foreach( $logLines as $line )
{
    echo $line;
}

Backing Up the Database

SQLite Backup

# Copy SQLite database file
cp database/cms.db database/backups/cms-$(date +%Y%m%d-%H%M%S).db

# Or use SQLite backup command
sqlite3 database/cms.db ".backup database/backups/cms-backup.db"

MySQL Backup

# Dump database
mysqldump -u cms_user -p cms_database > backups/cms-$(date +%Y%m%d-%H%M%S).sql

# Dump with compression
mysqldump -u cms_user -p cms_database | gzip > backups/cms-$(date +%Y%m%d-%H%M%S).sql.gz

PostgreSQL Backup

# Dump database
pg_dump -U cms_user cms_database > backups/cms-$(date +%Y%m%d-%H%M%S).sql

# Dump with compression
pg_dump -U cms_user cms_database | gzip > backups/cms-$(date +%Y%m%d-%H%M%S).sql.gz

Automated Backup Script

Create scripts/backup-db.sh:

#!/bin/bash
BACKUP_DIR="/path/to/backups"
DATE=$(date +%Y%m%d-%H%M%S)
DB_TYPE="mysql"  # or sqlite, postgresql

mkdir -p "$BACKUP_DIR"

case $DB_TYPE in
    sqlite)
        cp database/cms.db "$BACKUP_DIR/cms-$DATE.db"
        ;;
    mysql)
        mysqldump -u cms_user -p'password' cms_database | gzip > "$BACKUP_DIR/cms-$DATE.sql.gz"
        ;;
    postgresql)
        pg_dump -U cms_user cms_database | gzip > "$BACKUP_DIR/cms-$DATE.sql.gz"
        ;;
esac

# Keep only last 30 days of backups
find "$BACKUP_DIR" -name "cms-*.db" -mtime +30 -delete
find "$BACKUP_DIR" -name "cms-*.sql.gz" -mtime +30 -delete

echo "Backup completed: cms-$DATE"

Add to crontab:

# Daily backup at 2 AM
0 2 * * * /path/to/scripts/backup-db.sh >> /path/to/logs/backup.log 2>&1

Restoring from Backup

SQLite Restore

# Stop web server
sudo systemctl stop nginx

# Restore database
cp database/backups/cms-20250128-020000.db database/cms.db

# Restart web server
sudo systemctl start nginx

MySQL Restore

# Restore from SQL dump
mysql -u cms_user -p cms_database < backups/cms-20250128-020000.sql

# Restore from compressed dump
gunzip < backups/cms-20250128-020000.sql.gz | mysql -u cms_user -p cms_database

PostgreSQL Restore

# Restore from SQL dump
psql -U cms_user cms_database < backups/cms-20250128-020000.sql

# Restore from compressed dump
gunzip < backups/cms-20250128-020000.sql.gz | psql -U cms_user cms_database

Development Tasks

Creating a Custom Controller

Generate Controller Scaffold

./vendor/bin/neuron generate:controller DashboardController

Manual Controller Creation

Create src/Cms/Controllers/Custom/DashboardController.php:

<?php
namespace Neuron\Cms\Controllers\Custom;

use Neuron\Cms\Controllers\Base;
use Neuron\Mvc\Requests\Request;
use Neuron\Cms\Services\Auth\Authentication;
use Neuron\Cms\Repositories\IPostRepository;

class DashboardController extends Base
{
    private Authentication $_auth;
    private IPostRepository $_postRepo;

    public function __construct( Authentication $auth,
        IPostRepository $postRepo )
    {
        $this->_auth = $auth;
        $this->_postRepo = $postRepo;
    }

    public function index( Request $request ): string
    {
        // Check authentication
        if( !$this->_auth->check() )
        {
            return $this->redirect( '/login' );
        }

        $user = $this->_auth->user();

        // Get user's posts
        $posts = $this->_postRepo->findByAuthorId( $user->getId() );

        // Render view
        return $this->renderHtml( 200, [
            'user' => $user,
            'posts' => $posts,
            'title' => 'My Dashboard'
        ], 'dashboard/index' );
    }

    public function stats( Request $request ): string
    {
        if( !$this->_auth->isAdmin() )
        {
            return $this->renderJson( 403, ['error' => 'Forbidden'] );
        }

        $stats = [
            'total_posts' => $this->_postRepo->count(),
            'published' => $this->_postRepo->countByStatus( 'published' ),
            'drafts' => $this->_postRepo->countByStatus( 'draft' )
        ];

        return $this->renderJson( 200, $stats );
    }
}

Adding Custom Routes

Define Routes with Attributes

Add route attributes directly to your controller methods:

<?php

namespace App\Controllers;

use Neuron\Mvc\Controller;
use Neuron\Mvc\Request;
use Neuron\Routing\Attributes\Get;

class Dashboard extends Controller
{
    #[Get('/dashboard', name: 'dashboard.index', filters: ['auth'])]
    public function index(Request $request): string
    {
        return $this->renderHtml(OK, [], 'dashboard/index');
    }

    #[Get('/dashboard/stats', name: 'dashboard.stats', filters: ['auth', 'admin'])]
    public function stats(Request $request): string
    {
        return $this->renderJson(OK, ['stats' => $this->getStats()]);
    }
}
<?php

namespace App\Controllers\Api;

use Neuron\Mvc\Controller;
use Neuron\Mvc\Request;
use Neuron\Routing\Attributes\Get;

class Posts extends Controller
{
    #[Get('/api/posts/:id', name: 'api.posts.show', filters: ['api_limit'])]
    public function show(Request $request): string
    {
        $id = $request->getRouteParameter('id');
        $post = $this->postRepository->findById($id);
        return $this->renderJson(OK, ['post' => $post]);
    }
}

Register Controllers

Add your controllers to config/routing.yaml:

controller_paths:
  - path: 'app/Controllers'
    namespace: 'App\Controllers'
  - path: 'vendor/neuron-php/cms/src/Cms/Controllers'
    namespace: 'Neuron\Cms\Controllers'

Parameter Handling

// Route: /posts/:id/comments/:commentId
#[Get('/posts/:id/comments/:commentId', name: 'posts.comments.show')]
public function showComment(Request $request): string
{
    $postId = $request->getRouteParameter('id');
    $commentId = $request->getRouteParameter('commentId');

    // Find post and comment
    $post = $this->_postRepo->findById( $postId );
    $comment = $this->_commentRepo->findById( $commentId );

    return $this->renderJson( 200, [
        'post' => $post->toArray(),
        'comment' => $comment->toArray()
    ]);
}

Wildcard Routes

// Route: /docs/*path
public function docs( Request $request ): string
{
    $path = $request->getRouteParameter( 'path' );
    // $path could be "getting-started" or "api/authentication" or "guides/installation"

    $filePath = $this->_basePath . '/docs/' . $path . '.md';

    if( !file_exists( $filePath ) )
    {
        return $this->render404( $request );
    }

    $content = file_get_contents( $filePath );

    return $this->renderMarkdown( 200, [
        'content' => $content,
        'title' => ucwords( str_replace( '-', ' ', basename( $path ) ) )
    ]);
}

Creating a New Service

Service Class Structure

Create src/Cms/Services/Custom/AnalyticsService.php:

<?php
namespace Neuron\Cms\Services\Custom;

use Neuron\Cms\Repositories\IPostRepository;
use Neuron\Cms\Repositories\IUserRepository;

class AnalyticsService
{
    private IPostRepository $_postRepo;
    private IUserRepository $_userRepo;

    public function __construct( IPostRepository $postRepo,
        IUserRepository $userRepo )
    {
        $this->_postRepo = $postRepo;
        $this->_userRepo = $userRepo;
    }

    /**
     * Get dashboard statistics
     */
    public function getDashboardStats(): array
    {
        return [
            'total_posts' => $this->_postRepo->count(),
            'published_posts' => $this->_postRepo->countByStatus( 'published' ),
            'draft_posts' => $this->_postRepo->countByStatus( 'draft' ),
            'total_users' => $this->_userRepo->count(),
            'active_authors' => $this->getActiveAuthorCount(),
            'total_views' => $this->getTotalViews()
        ];
    }

    /**
     * Get most viewed posts
     */
    public function getMostViewedPosts( int $limit = 10 ): array
    {
        return $this->_postRepo->findMostViewed( $limit );
    }

    /**
     * Get author statistics
     */
    public function getAuthorStats( int $authorId ): array
    {
        $posts = $this->_postRepo->findByAuthorId( $authorId );

        $published = 0;
        $totalViews = 0;

        foreach( $posts as $post )
{
            if( $post->getStatus() === 'published' )
            {
                $published++;
            }
            $totalViews += $post->getViewCount();
        }

        return [
            'total_posts' => count( $posts ),
            'published_posts' => $published,
            'total_views' => $totalViews,
            'avg_views_per_post' => count( $posts ) > 0 ? $totalViews / count( $posts ) : 0
        ];
    }

    private function getActiveAuthorCount(): int
    {
        // Count users with at least one published post
        $authors = $this->_userRepo->findByRole( 'author' );
        $active = 0;

        foreach( $authors as $author )
{
            $posts = $this->_postRepo->findByAuthorId( $author->getId() );
            foreach( $posts as $post )
{
                if( $post->getStatus() === 'published' )
                {
                    $active++;
                    break;
                }
            }
        }

        return $active;
    }

    private function getTotalViews(): int
    {
        $posts = $this->_postRepo->findAll();
        $total = 0;

        foreach( $posts as $post )
{
            $total += $post->getViewCount();
        }

        return $total;
    }
}

Using the Service

// In controller
$analytics = new \Neuron\Cms\Services\Custom\AnalyticsService( $postRepo, $userRepo );
$stats = $analytics->getDashboardStats();

Implementing Event Listeners

Create Event Class

Create src/Cms/Events/Custom/PostViewedEvent.php:

<?php
namespace Neuron\Cms\Events\Custom;

use Neuron\Events\IEvent;
use Neuron\Cms\Models\Post;

class PostViewedEvent implements IEvent
{
    private Post $_post;
    private string $_ipAddress;
    private string $_userAgent;
    private float $_timestamp;

    public function __construct( Post $post,
        string $ipAddress,
        string $userAgent,
        float $timestamp )
    {
        $this->_post = $post;
        $this->_ipAddress = $ipAddress;
        $this->_userAgent = $userAgent;
        $this->_timestamp = $timestamp;
    }

    public function getPost(): Post
    {
        return $this->_post;
    }

    public function getIpAddress(): string
    {
        return $this->_ipAddress;
    }

    public function getUserAgent(): string
    {
        return $this->_userAgent;
    }

    public function getTimestamp(): float
    {
        return $this->_timestamp;
    }
}

Create Listener

Create src/Cms/Listeners/Custom/TrackPostViewListener.php:

<?php
namespace Neuron\Cms\Listeners\Custom;

use Neuron\Events\IListener;
use Neuron\Events\IEvent;
use Neuron\Cms\Events\Custom\PostViewedEvent;
use Neuron\Cms\Repositories\IPostRepository;
use Neuron\Log\Log;

class TrackPostViewListener implements IListener
{
    private IPostRepository $_postRepo;

    public function __construct( IPostRepository $postRepo )
    {
        $this->_postRepo = $postRepo;
    }

    public function handle( IEvent $event ): void
    {
        if( !$event instanceof PostViewedEvent )
{
            return;
        }

        $post = $event->getPost();

        // Increment view count
        $post->incrementViewCount();
        $this->_postRepo->update( $post );

        // Log for analytics
        Log::info( "Post viewed: {$post->getSlug()} from {$event->getIpAddress()}" );
    }
}

Register Listener

In config/events.yaml:

events:
  Neuron\Cms\Events\Custom\PostViewedEvent:
    - Neuron\Cms\Listeners\Custom\TrackPostViewListener

Emit Event

use Neuron\Application\CrossCutting\Event;
use Neuron\Cms\Events\Custom\PostViewedEvent;

// In post display controller
Event::emit( new PostViewedEvent( $post,
    $_SERVER['REMOTE_ADDR'] ?? 'unknown',
    $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
    microtime( true )
) );

Extending the User Model

Add Custom Properties

<?php
namespace App\Models;

use Neuron\Cms\Models\User as BaseUser;

class User extends BaseUser
{
    private ?string $_bio = null;
    private ?string $_website = null;
    private ?string $_socialTwitter = null;
    private ?string $_socialGithub = null;

    // Bio property
    public function getBio(): ?string
    {
        return $this->_bio;
    }

    public function setBio( ?string $bio ): self
    {
        $this->_bio = $bio;
        return $this;
    }

    // Website property
    public function getWebsite(): ?string
    {
        return $this->_website;
    }

    public function setWebsite( ?string $website ): self
    {
        $this->_website = $website;
        return $this;
    }

    // Social media properties
    public function getSocialTwitter(): ?string
    {
        return $this->_socialTwitter;
    }

    public function setSocialTwitter( ?string $twitter ): self
    {
        $this->_socialTwitter = $twitter;
        return $this;
    }

    public function getSocialGithub(): ?string
    {
        return $this->_socialGithub;
    }

    public function setSocialGithub( ?string $github ): self
    {
        $this->_socialGithub = $github;
        return $this;
    }

    // Override toArray to include custom fields
    public function toArray(): array
    {
        $data = parent::toArray();

        $data['bio'] = $this->_bio;
        $data['website'] = $this->_website;
        $data['social_twitter'] = $this->_socialTwitter;
        $data['social_github'] = $this->_socialGithub;

        return $data;
    }

    // Override fromArray to load custom fields
    public function fromArray( array $data ): void
    {
        parent::fromArray( $data );

        $this->_bio = $data['bio'] ?? null;
        $this->_website = $data['website'] ?? null;
        $this->_socialTwitter = $data['social_twitter'] ?? null;
        $this->_socialGithub = $data['social_github'] ?? null;
    }
}

Create Migration for Custom Fields

./vendor/bin/neuron db:migration:generate AddCustomFieldsToUsers

Edit generated migration:

<?php
use Phinx\Migration\AbstractMigration;

class AddCustomFieldsToUsers extends AbstractMigration
{
    public function change()
    {
        $table = $this->table( 'users' );

        $table->addColumn( 'bio', 'text', ['null' => true] )
              ->addColumn( 'website', 'string', ['limit' => 255, 'null' => true] )
              ->addColumn( 'social_twitter', 'string', ['limit' => 100, 'null' => true] )
              ->addColumn( 'social_github', 'string', ['limit' => 100, 'null' => true] )
              ->update();
    }
}

Run migration:

./vendor/bin/neuron db:migrate

Creating Database Migrations

See Database Migrations Guide for complete documentation.

Quick Reference

# Create migration
./vendor/bin/neuron db:migration:generate CreateCommentsTable

# Run migrations
./vendor/bin/neuron db:migrate

# Rollback last migration
./vendor/bin/neuron db:rollback

# Check migration status
./vendor/bin/neuron db:migrate:status

Deployment Tasks

Deploying to Production

Pre-Deployment Checklist

  1. Backup Database

    php scripts/backup-db.sh
    
  2. Run Tests

    ./vendor/bin/phpunit tests
    
  3. Check Code Quality

    vendor/bin/phpmd src text cleancode,design,naming
    vendor/bin/phpcs --standard=PSR12 src
    
  4. Build Assets (if applicable)

    npm run build
    
  5. Review Configuration

    • Database credentials
    • Email settings
    • Debug mode disabled
    • HTTPS enabled

Deployment Steps

  1. Pull Latest Code

    cd /var/www/cms
    git pull origin main
    
  2. Update Dependencies

    composer install --no-dev --optimize-autoloader
    
  3. Run Migrations

    ./vendor/bin/neuron db:migrate
    
  4. Clear Cache

    ./vendor/bin/neuron cache:clear
    
  5. Set Permissions

    chown -R www-data:www-data /var/www/cms
    chmod -R 755 /var/www/cms
    chmod -R 775 /var/www/cms/storage
    chmod -R 775 /var/www/cms/logs
    
  6. Restart Services

    sudo systemctl restart nginx
    sudo systemctl restart php8.2-fpm
    

Automated Deployment Script

Create scripts/deploy.sh:

#!/bin/bash
set -e

echo "Starting deployment..."

# Pull latest code
git pull origin main

# Update dependencies
composer install --no-dev --optimize-autoloader

# Run migrations
./vendor/bin/neuron db:migrate

# Clear cache
./vendor/bin/neuron cache:clear

# Set permissions
chown -R www-data:www-data .
chmod -R 755 .
chmod -R 775 storage logs

# Restart services
sudo systemctl restart php8.2-fpm
sudo systemctl restart nginx

echo "Deployment completed successfully!"

Setting Up SSL/TLS

Let's Encrypt (Certbot)

# Install certbot
sudo apt-get install certbot python3-certbot-nginx

# Obtain certificate
sudo certbot --nginx -d example.com -d www.example.com

# Test renewal
sudo certbot renew --dry-run

Nginx Configuration for SSL

Edit /etc/nginx/sites-available/cms:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    root /var/www/cms/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
}

Configuring Web Server

Apache Virtual Host

Create /etc/apache2/sites-available/cms.conf:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/cms/public

    <Directory /var/www/cms/public>
        AllowOverride All
        Require all granted

        # Rewrite rules
        RewriteEngine On
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule ^ index.php [QSA,L]
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/cms_error.log
    CustomLog ${APACHE_LOG_DIR}/cms_access.log combined
</VirtualHost>

Enable site:

sudo a2ensite cms
sudo a2enmod rewrite
sudo systemctl reload apache2

Nginx Configuration

Create /etc/nginx/sites-available/cms:

server {
    listen 80;
    server_name example.com www.example.com;

    root /var/www/cms/public;
    index index.php index.html;

    access_log /var/log/nginx/cms_access.log;
    error_log /var/log/nginx/cms_error.log;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }

    # Cache static assets
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 365d;
        add_header Cache-Control "public, immutable";
    }
}

Enable site:

sudo ln -s /etc/nginx/sites-available/cms /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Setting Up Background Jobs

Systemd Service

Create /etc/systemd/system/cms-queue.service:

[Unit]
Description=CMS Queue Worker
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/cms
ExecStart=/usr/bin/php /var/www/cms/vendor/bin/neuron queue:work
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable cms-queue
sudo systemctl start cms-queue
sudo systemctl status cms-queue

Supervisor Configuration

Create /etc/supervisor/conf.d/cms-queue.conf:

[program:cms-queue]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/cms/vendor/bin/neuron queue:work
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/cms/logs/worker.log

Update supervisor:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start cms-queue:*

Performance Optimization

Enable OPcache

Edit /etc/php/8.2/fpm/conf.d/10-opcache.ini:

opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0  # Disable in production
opcache.revalidate_freq=0
opcache.fast_shutdown=1

Enable View Caching

In config/neuron.yaml:

cache:
  enabled: true
  type: file
  ttl: 3600
  path: storage/cache

Database Query Optimization

// Use eager loading to reduce queries
$posts = $postRepo->findAll();
foreach( $posts as $post )
{
    $post->loadCategories();  // N+1 query problem
}

// Better: Load all at once
$posts = $postRepo->findAllWithRelationships();

Enable Gzip Compression (Nginx)

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss;

Integration Tasks

Integrating Third-Party Services

API Client Pattern

<?php
namespace App\Services;

class WeatherApiClient
{
    private string $_apiKey;
    private string $_baseUrl = 'https://api.weather.com/v1';

    public function __construct( string $apiKey )
    {
        $this->_apiKey = $apiKey;
    }

    public function getCurrentWeather( string $city ): array
    {
        $url = $this->_baseUrl . '/current';
        $params = [
            'city' => $city,
            'apikey' => $this->_apiKey
        ];

        $ch = curl_init( $url . '?' . http_build_query( $params ) );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        curl_setopt( $ch, CURLOPT_TIMEOUT, 10 );

        $response = curl_exec( $ch );
        $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
        curl_close( $ch );

        if( $httpCode !== 200 )
{
            throw new \Exception( "API request failed with status {$httpCode}" );
        }

        return json_decode( $response, true );
    }
}

Using in Controller

public function weather( Request $request ): string
{
    $apiKey = $this->_settings->get( 'weather', 'api_key' );
    $client = new WeatherApiClient( $apiKey );

    try
    {
        $weather = $client->getCurrentWeather( 'London' );

        return $this->renderJson( 200, $weather );
    }
    catch( Exception $e )
{
        return $this->renderJson( 500, [
            'error' => $e->getMessage()
        ]);
    }
}

Setting Up OAuth Authentication

OAuth integration would require additional packages and configuration. The CMS currently supports username/password and remember me authentication.

For OAuth integration, consider using league/oauth2-client:

composer require league/oauth2-client

GitHub OAuth Example

use League\OAuth2\Client\Provider\Github;

$provider = new Github( [
    'clientId'     => '{github-client-id}',
    'clientSecret' => '{github-client-secret}',
    'redirectUri'  => 'https://example.com/oauth/github/callback'
] );

// Get authorization URL
$authUrl = $provider->getAuthorizationUrl();
$_SESSION['oauth2state'] = $provider->getState();
header( 'Location: ' . $authUrl );
exit;

// Handle callback
if( isset( $_GET['code'] ) )
{
    $token = $provider->getAccessToken( 'authorization_code', [
        'code' => $_GET['code']
    ] );

    $user = $provider->getResourceOwner( $token );

    // Create or update user in CMS
    // ...
}

Configuring CDN

Cloudflare Setup

  1. Add domain to Cloudflare
  2. Update DNS nameservers
  3. Enable caching rules
  4. Configure page rules for static assets

CDN URL Rewriting

In config/neuron.yaml:

cdn:
  enabled: true
  url: https://cdn.example.com

In templates:

function assetUrl( $path )
{
    $cdnUrl = Registry::getInstance()->get( 'CDN.Url' );
    if( $cdnUrl )
{
        return $cdnUrl . '/' . ltrim( $path, '/' );
    }
    return '/' . ltrim( $path, '/' );
}

// Usage
echo '<img src="' . assetUrl( 'images/logo.png' ) . '">';
// Output: <img src="https://cdn.example.com/images/logo.png">

Additional Resources