⚠️ 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.
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.
admin_jane[email protected]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();
}
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!
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]' );
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";
}
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!
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";
$updater->update( $user,
$user->getUsername(),
$user->getEmail(),
UserRole::Author, // Demote to author
null,
$user->getTimezone()
);
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()}";
$user = $userRepo->findByUsername( 'restored_user' );
$user->setStatus( User::STATUS_ACTIVE );
$userRepo->update( $user );
echo "User reactivated: {$user->getUsername()}";
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
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";
}
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()}";
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();
}
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
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 );
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
);
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' );
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 );
// 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 );
}
}
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();
}
use Neuron\Cms\Models\Post;
// Mark as inactive/deleted without removing from database
$post->setStatus( 'deleted' );
$postRepo->update( $post );
use Neuron\Cms\Services\Post\Deleter;
$deleter = new Deleter( $postRepo );
// Permanently delete post
$deleter->delete( $post );
// Or delete by ID
$deleter->deleteById( 123 );
// 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 );
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();
}
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]
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>
$sender->to( $user->getEmail(), $user->getUsername() )
->subject( "Welcome to {$siteName}" )
->template( 'emails/custom-welcome', [
'SiteName' => $siteName,
'Username' => $user->getUsername(),
'ActivationLink' => $activationUrl
])
->send();
Available variables depend on the template context:
Password Reset Template:
$ResetLink: Password reset URL$ExpirationMinutes: Token expiration time$SiteName: Site name from settingsEmail Verification Template:
$VerificationLink: Email verification URL$ExpirationMinutes: Token expiration time$SiteName: Site name from settings$Username: User's usernameIn config/neuron.yaml:
email:
test_mode: true # Logs emails instead of sending
When enabled, check logs:
tail -f logs/application.log | grep "TEST MODE"
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}" );
};
# Test SMTP connection manually
telnet smtp.example.com 587
Or use:
openssl s_client -connect smtp.example.com:465
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
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
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;
}
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 the CMS to a newer version involves updating the package via Composer and running the upgrade command.
# 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
# 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.
# 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
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
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
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.
# Clear all cache
./vendor/bin/neuron cache:clear
# Check cache statistics
./vendor/bin/neuron cache:stats
use Neuron\Mvc\Cache\ViewCache;
$cache = Registry::getInstance()->get( 'ViewCache' );
if( $cache instanceof ViewCache )
{
$cleared = $cache->clear();
echo "Cleared {$cleared} cache entries";
}
$removed = $app->clearExpiredCache();
echo "Removed {$removed} expired cache entries";
logs/
├── application.log # General application logs
├── error.log # Error logs
├── access.log # HTTP access logs
└── scheduled-publish.log # Scheduled task 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
// 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;
}
# 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"
# 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
# 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
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
# 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
# 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
# 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
./vendor/bin/neuron generate:controller DashboardController
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 );
}
}
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]);
}
}
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'
// 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()
]);
}
// 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 ) ) )
]);
}
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;
}
}
// In controller
$analytics = new \Neuron\Cms\Services\Custom\AnalyticsService( $postRepo, $userRepo );
$stats = $analytics->getDashboardStats();
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 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()}" );
}
}
In config/events.yaml:
events:
Neuron\Cms\Events\Custom\PostViewedEvent:
- Neuron\Cms\Listeners\Custom\TrackPostViewListener
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 )
) );
<?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;
}
}
./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
See Database Migrations Guide for complete documentation.
# 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
Backup Database
php scripts/backup-db.sh
Run Tests
./vendor/bin/phpunit tests
Check Code Quality
vendor/bin/phpmd src text cleancode,design,naming
vendor/bin/phpcs --standard=PSR12 src
Build Assets (if applicable)
npm run build
Review Configuration
Pull Latest Code
cd /var/www/cms
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 /var/www/cms
chmod -R 755 /var/www/cms
chmod -R 775 /var/www/cms/storage
chmod -R 775 /var/www/cms/logs
Restart Services
sudo systemctl restart nginx
sudo systemctl restart php8.2-fpm
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!"
# 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
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;
}
}
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
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
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
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:*
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
In config/neuron.yaml:
cache:
enabled: true
type: file
ttl: 3600
path: storage/cache
// 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();
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;
<?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 );
}
}
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()
]);
}
}
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
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
// ...
}
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">