This guide provides solutions to common issues encountered when installing, configuring, and operating the Neuron CMS. Each section includes symptoms, possible causes, diagnostic steps, and solutions.
Symptom: Composer fails to install CMS package
Your requirements could not be resolved to an installable set of packages.
Possible Causes:
Solutions:
php -v
Ensure PHP 8.4 or higher is installed. If not:
# Ubuntu/Debian
sudo apt-get install php8.4-cli php8.4-mbstring php8.4-xml php8.4-curl
# macOS (Homebrew)
brew install [email protected]
# Update alternatives
sudo update-alternatives --set php /usr/bin/php8.4
# Temporary increase
php -d memory_limit=512M /usr/local/bin/composer install
# Or edit php.ini
memory_limit = 512M
php -m | grep -E 'pdo|mbstring|xml|curl|json'
Required extensions:
Install missing extensions:
# Ubuntu/Debian
sudo apt-get install php8.4-pdo php8.4-mbstring php8.4-xml php8.4-curl
# macOS (usually included)
composer clear-cache
composer install
# Test Packagist connectivity
curl -I https://packagist.org
# Try installing with verbose output
composer install -vvv
# See what's causing conflicts
composer why-not neuron-php/cms
# Try updating dependencies
composer update --with-all-dependencies
Symptom: Cannot write to storage directory
file_put_contents(/path/to/storage/file): failed to open stream: Permission denied
Solutions:
ls -la storage/
ls -la logs/
# Make directories writable
chmod -R 775 storage logs
# Set correct ownership (web server user)
sudo chown -R www-data:www-data storage logs
# Or for current user
sudo chown -R $USER:www-data storage logs
# Apache
ps aux | grep apache | head -1
# Nginx
ps aux | grep nginx | head -1
# Common users: www-data (Ubuntu/Debian), apache (CentOS), _www (macOS)
# Set ACL permissions
sudo setfacl -R -m u:www-data:rwx storage logs
sudo setfacl -dR -m u:www-data:rwx storage logs
mkdir -p storage/cache storage/sessions storage/uploads
mkdir -p logs
chmod -R 775 storage logs
Symptom: Database migrations fail to execute
Migration failed: SQLSTATE[42S01]: Base table or view already exists
Solutions:
./vendor/bin/neuron db:migrate:status
# Run with verbose output
./vendor/bin/neuron db:migrate -vvv
# Rollback last migration
./vendor/bin/neuron db:rollback
# Or mark as migrated without running
# Edit phinxlog table directly in database
# Validate PHP syntax
php -l db/migrate/20250128120000_create_users.php
# If syntax error, edit migration file and fix
# Test database connection
php -r "new PDO('mysql:host=localhost;dbname=cms', 'user', 'pass');"
-- Check user permissions
SHOW GRANTS FOR 'cms_user'@'localhost';
-- Grant necessary permissions
GRANT CREATE, ALTER, DROP ON cms_database.* TO 'cms_user'@'localhost';
# Clear migration history
# WARNING: Only for development!
# SQLite
sqlite3 database/cms.db "DELETE FROM phinxlog;"
# MySQL
mysql -u root -p cms_database -e "DELETE FROM phinxlog;"
# Re-run migrations
./vendor/bin/neuron db:migrate
Symptom: Unable to connect to SQLite database
SQLSTATE[HY000]: General error: 14 unable to open database file
Solutions:
In config/neuron.yaml:
database:
adapter: sqlite
name: database/cms.db # Relative to base path
ls -la database/cms.db
# File must be writable by web server
chmod 664 database/cms.db
chmod 775 database/
# Set ownership
sudo chown www-data:www-data database/cms.db
touch database/cms.db
chmod 664 database/cms.db
df -h
# Check if SELinux is enforcing
sestatus
# Allow httpd to write to database
sudo chcon -R -t httpd_sys_rw_content_t database/
Symptom: PDO connection exception for MySQL
SQLSTATE[HY000] [2002] Connection refused
Solutions:
# Check service status
sudo systemctl status mysql
# or
sudo systemctl status mariadb
# Start if not running
sudo systemctl start mysql
mysql -h localhost -u cms_user -p
In config/neuron.yaml:
database:
adapter: mysql
host: localhost # or 127.0.0.1
port: 3306
name: cms_database
user: cms_user
pass: password
SELECT User, Host FROM mysql.user WHERE User='cms_user';
SHOW GRANTS FOR 'cms_user'@'localhost';
-- If user exists for 'localhost' but you're connecting via '127.0.0.1'
CREATE USER 'cms_user'@'127.0.0.1' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON cms_database.* TO 'cms_user'@'127.0.0.1';
FLUSH PRIVILEGES;
# Check if MySQL port is accessible
telnet localhost 3306
# or
nc -zv localhost 3306
Edit /etc/mysql/mysql.conf.d/mysqld.cnf:
bind-address = 127.0.0.1
Restart MySQL:
sudo systemctl restart mysql
Symptom: Cannot connect to PostgreSQL database
SQLSTATE[08006] could not connect to server
Solutions:
sudo systemctl status postgresql
psql -h localhost -U cms_user -d cms_database
Edit /etc/postgresql/14/main/pg_hba.conf:
# Allow local connections
local all all peer
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
Reload configuration:
sudo systemctl reload postgresql
Edit /etc/postgresql/14/main/postgresql.conf:
listen_addresses = 'localhost'
port = 5432
sudo -u postgres psql
CREATE USER cms_user WITH PASSWORD 'password';
CREATE DATABASE cms_database OWNER cms_user;
GRANT ALL PRIVILEGES ON DATABASE cms_database TO cms_user;
Symptom: SQL error indicating missing table
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'cms_database.users' doesn't exist
Solutions:
# Check migration status
./vendor/bin/neuron db:migrate:status
# Run pending migrations
./vendor/bin/neuron db:migrate
-- MySQL
SHOW TABLES;
-- PostgreSQL
\dt
-- SQLite
.tables
Ensure your code uses the correct table name (case-sensitive on Linux):
// Correct
$stmt = $db->prepare( "SELECT * FROM users" );
// Incorrect (if table is 'users' not 'Users')
$stmt = $db->prepare( "SELECT * FROM Users" );
# Rollback all migrations
./vendor/bin/neuron db:rollback --target=0
# Re-run all migrations
./vendor/bin/neuron db:migrate
Symptom: Valid credentials rejected
Possible Causes:
Solutions:
SELECT id, username, email, failed_login_attempts, locked_until
FROM users
WHERE email = '[email protected]';
If locked_until is in the future, unlock manually:
UPDATE users
SET failed_login_attempts = 0, locked_until = NULL
WHERE email = '[email protected]';
Or use CLI script:
php scripts/unlock-user.php [email protected]
php scripts/reset-password.php [email protected] NewPassword123!
SELECT * FROM users WHERE email = '[email protected]';
<?php
$hash = '$argon2id$...'; // From database
$password = 'test123';
if( password_verify( $password, $hash ) ) {
echo "Password matches\n";
}
else {
echo "Password does not match\n";
}
In php.ini:
session.cookie_httponly = 1
session.cookie_secure = 0 ; Set to 1 for HTTPS only
session.use_strict_mode = 1
// In Authentication service
Log::debug( "Login attempt", [
'username' => $username,
'user_found' => $user ? 'yes' : 'no',
'password_valid' => $this->_passwordHasher->verify( $password, $user->getPasswordHash() ),
'is_locked' => $user->isLockedOut()
]);
# Check logs for database errors
tail -f logs/application.log | grep ERROR
Symptom: User logged out immediately after login
Solutions:
# Check where sessions are stored
php -r "echo ini_get('session.save_path');"
# Verify directory is writable
ls -ld $(php -r "echo ini_get('session.save_path');")
# Default location
sudo chmod 1733 /var/lib/php/sessions
# or
sudo chmod 1733 /tmp
In php.ini:
session.cookie_lifetime = 0 ; Session cookie (closes with browser)
session.cookie_path = /
session.cookie_domain = ; Leave empty for current domain
session.cookie_secure = 0 ; Set to 1 if using HTTPS
session.cookie_httponly = 1
session.cookie_samesite = Lax
// In Authentication::login()
session_regenerate_id( true ); // Ensure this is called
// Verify session is being written
$_SESSION['user_id'] = $user->getId();
session_write_close(); // Force write
If accessing via www.example.com and example.com, sessions may not persist:
// Set consistent domain
ini_set( 'session.cookie_domain', '.example.com' ); // Note the dot
// test-session.php
<?php
session_start();
if( !isset( $_SESSION['test'] ) ) {
$_SESSION['test'] = time();
echo "Session created: " . $_SESSION['test'];
}
else {
echo "Session persisted: " . $_SESSION['test'];
}
Visit page twice - should show "persisted" on second visit.
Symptom: Form submission rejected with CSRF error
CSRF validation failed
Solutions:
$csrf = new CsrfToken( $sessionManager );
$token = $csrf->getToken();
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars( $token ) ?>">
<!-- other fields -->
</form>
// Ensure session is started before generating token
if( session_status() === PHP_SESSION_NONE ) {
session_start();
}
var_dump( $_POST['csrf_token'] );
var_dump( $_SESSION['csrf_token'] );
If user opens form in multiple tabs, tokens may not match. Solution: Generate token per form, not per session.
If form is left open too long, session may expire:
session.gc_maxlifetime = 7200 ; 2 hours
Symptom: Email delivery failures
SMTP Error: Could not connect to SMTP host
Solutions:
In config/neuron.yaml:
email:
test_mode: true # Logs instead of sending
Check logs:
tail -f logs/application.log | grep "TEST MODE"
email:
driver: smtp
host: smtp.gmail.com
port: 587
username: [email protected]
password: your-app-password
encryption: tls
# Using telnet
telnet smtp.gmail.com 587
# Using OpenSSL
openssl s_client -connect smtp.gmail.com:587 -starttls smtp
# Test port 587
nc -zv smtp.gmail.com 587
# If blocked, check firewall
sudo ufw status
$mail->SMTPDebug = 2;
$mail->Debugoutput = function( $str ) {
Log::debug( "SMTP: {$str}" );
};
# Try port 465 with SSL
email:
port: 465
encryption: ssl
# Or try port 25 (if allowed)
email:
port: 25
encryption: null
Symptom: Authentication error with Gmail SMTP
SMTP Error: Could not authenticate
Solutions:
Go to Google Account > Security > App Passwords
Generate app password and use in config:
email:
username: [email protected]
password: abcd efgh ijkl mnop # 16-character app password
If app passwords don't work (shouldn't be needed):
Visit https://myaccount.google.com/lesssecureapps
Visit https://myaccount.google.com/notifications
Verify no security alerts blocking the connection.
For production, consider implementing OAuth 2.0 authentication instead of username/password.
Symptom: Emails arrive late or not at all
Solutions:
# Check queue worker status
ps aux | grep "queue:work"
# Check queue size
# Implementation depends on queue driver
php vendor/bin/neuron queue:work --once
# If using local SMTP (postfix/sendmail)
tail -f /var/log/mail.log
Emails might be marked as spam. Verify:
SMTP providers may rate limit. Check provider's sending limits.
Symptom: Application responds slowly (>2 seconds)
Solutions:
In config/neuron.yaml:
cache:
enabled: true
type: file
ttl: 3600
path: storage/cache
In php.ini:
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
Restart PHP-FPM:
sudo systemctl restart php8.4-fpm
; php.ini
zend_extension=xdebug.so
xdebug.mode=profile
xdebug.output_dir=/tmp/xdebug
Analyze with tools like KCacheGrind or Webgrind.
# Nginx - enable gzip compression
gzip on;
gzip_types text/css application/javascript application/json;
# Enable FastCGI cache
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=cms:100m max_size=1g;
Add indexes on frequently queried columns:
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_posts_published_at ON posts(published_at);
Implement application-level query caching for expensive queries.
Symptom: Slow database queries (>100ms)
Solutions:
MySQL:
-- Enable slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.1; -- 100ms
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-query.log';
# View slow queries
sudo tail -f /var/log/mysql/slow-query.log
# Use pt-query-digest (Percona Toolkit)
sudo pt-query-digest /var/log/mysql/slow-query.log
EXPLAIN SELECT * FROM posts
WHERE status = 'published'
ORDER BY published_at DESC
LIMIT 10;
Look for:
-- If EXPLAIN shows full table scan on status
CREATE INDEX idx_posts_status_published ON posts(status, published_at);
-- Inefficient
SELECT * FROM posts p, users u WHERE p.author_id = u.id;
-- Better
SELECT * FROM posts p
INNER JOIN users u ON p.author_id = u.id
WHERE p.status = 'published';
Instead of N+1 queries:
// Bad: N+1 queries
$posts = $postRepo->findAll();
foreach( $posts as $post )
{
$author = $post->getAuthor(); // Separate query each time
}
// Good: Eager loading
$posts = $postRepo->findAllWithAuthors(); // Single JOIN query
Symptom: PHP memory limit exceeded
Fatal error: Allowed memory size of X bytes exhausted
Solutions:
In php.ini:
memory_limit = 256M ; or 512M for larger applications
Or in script:
ini_set( 'memory_limit', '256M' );
// Bad: Load all posts into memory
$posts = $postRepo->findAll(); // Could be millions
// Good: Process in batches
$offset = 0;
$limit = 100;
while( true )
{
$posts = $postRepo->findAll( $limit, $offset );
if( empty( $posts ) ) {
break;
}
foreach( $posts as $post )
{
// Process post
}
$offset += $limit;
unset( $posts ); // Free memory
gc_collect_cycles(); // Force garbage collection
}
public function streamAll(): \Generator
{
$stmt = $this->_db->query( "SELECT * FROM posts" );
while( $row = $stmt->fetch( PDO::FETCH_ASSOC ) ) {
yield $this->hydrate( $row );
}
}
// Usage
foreach( $postRepo->streamAll() as $post ) {
// Process one post at a time
// Memory usage remains constant
}
// Monitor memory usage
echo "Memory: " . memory_get_usage() / 1024 / 1024 . " MB\n";
// Check for circular references
gc_collect_cycles();
// Don't load entire image into memory
// Use ImageMagick or GD with resource limits
$image = new Imagick( $file );
$image->setResourceLimit( Imagick::RESOURCETYPE_MEMORY, 256 * 1024 * 1024 ); // 256MB
$image->setResourceLimit( Imagick::RESOURCETYPE_MAP, 512 * 1024 * 1024 ); // 512MB
Symptom: View file not found error
Template not found: admin/dashboard/index.php
Solutions:
Check config/neuron.yaml:
views:
path: resources/views
Verify file exists:
ls -la resources/views/admin/dashboard/index.php
Linux is case-sensitive:
// Wrong
$this->renderHtml( 200, [], 'Admin/Dashboard/Index' );
// Correct (if directory is lowercase)
$this->renderHtml( 200, [], 'admin/dashboard/index' );
$viewPath = Registry::getInstance()->get( 'Views.Path' );
echo "View path: {$viewPath}\n";
mkdir -p resources/views/admin/dashboard
touch resources/views/admin/dashboard/index.php
Symptom: PHP errors in template files
ParseError: syntax error, unexpected '}' in template.php
Solutions:
php -l resources/views/admin/dashboard/index.php
Undefined Variable:
// Template expects $user but not passed
<?php echo $user->getName(); ?> // Undefined variable: user
Fix in controller:
return $this->renderHtml( 200, [
'user' => $user // Pass variable
], 'admin/dashboard/index' );
Escaping Issues:
// Always escape output
<?= htmlspecialchars( $userInput, ENT_QUOTES, 'UTF-8' ) ?>
ini_set( 'display_errors', 1 );
error_reporting( E_ALL );
// In template, debug available variables
<?php var_dump( get_defined_vars() ); ?>
Symptom: Queued jobs remain pending
Solutions:
ps aux | grep "queue:work"
If not running, start worker:
./vendor/bin/neuron queue:work &
# Or with supervisor
sudo supervisorctl start cms-queue:*
# Process one job
./vendor/bin/neuron queue:work --once
# Process with verbose output
./vendor/bin/neuron queue:work --once -vvv
In config/neuron.yaml:
queue:
driver: database # or redis, beanstalkd
connection: default
SELECT * FROM jobs LIMIT 10;
SELECT * FROM failed_jobs LIMIT 10;
./vendor/bin/neuron queue:failed
# Retry failed job
./vendor/bin/neuron queue:retry <job-id>
# Retry all failed
./vendor/bin/neuron queue:retry --all
Symptom: Job worker terminates unexpectedly
Solutions:
tail -f logs/worker.log
tail -f logs/application.log | grep ERROR
# Supervisor configuration
[program:cms-queue]
command=php /var/www/cms/vendor/bin/neuron queue:work
autostart=true
autorestart=true # Restart if crashes
Worker may run out of memory. Add memory limit to worker:
./vendor/bin/neuron queue:work --memory=512
Increase timeout for long-running jobs:
./vendor/bin/neuron queue:work --timeout=300 # 5 minutes
public function handle(): void
{
try
{
// Job logic
}
catch( \Exception $e )
{
Log::error( "Job failed: " . $e->getMessage() );
throw $e; // Re-throw to mark as failed
}
}
Symptom: Cron-like tasks not running
Solutions:
# Ensure cron entry exists
crontab -l | grep schedule
Add if missing:
crontab -e
* * * * * cd /path/to/cms && php vendor/bin/neuron schedule:run >> /dev/null 2>&1
php vendor/bin/neuron schedule:run
Ensure tasks are registered in scheduler configuration.
sudo systemctl status cron
# or
sudo systemctl status crond
Symptom: Valid routes return 404
Solutions:
# List all routes
./vendor/bin/neuron routes:list
Verify your route is registered in a controller with attributes:
// app/Controllers/Dashboard.php
use Neuron\Routing\Attributes\Get;
class Dashboard extends Controller
{
#[Get('/dashboard', name: 'dashboard.index')]
public function index(Request $request): string
{
return $this->renderHtml(OK, [], 'dashboard/index');
}
}
Ensure the controller is registered in config/routing.yaml:
controller_paths:
- path: 'app/Controllers'
namespace: 'App\Controllers'
Nginx:
location / {
try_files $uri $uri/ /index.php?$query_string;
}
Apache (.htaccess):
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
Test rewrite:
# Apache
sudo apache2ctl -t
# Nginx
sudo nginx -t
If CMS is in subdirectory:
// In routes
route: /subdirectory/dashboard
// Or configure base path
$app->setBasePath( '/subdirectory' );
rm -rf storage/cache/routes*
Symptom: Controller class resolution failure
Class 'App\Controllers\DashboardController' not found
Solutions:
<?php
namespace App\Controllers; // Must match composer.json autoload
class DashboardController extends Base
{
// ...
}
In composer.json:
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
composer dump-autoload
# If namespace is App\Controllers\DashboardController
# File should be at: src/Controllers/DashboardController.php
ls -la src/Controllers/DashboardController.php
// File: DashboardController.php
class DashboardController // Must match filename
Symptom: Errors after package upgrade
Call to undefined method Neuron\Mvc\Application::newMethod()
Solutions:
# Check current versions
composer show neuron-php/cms
# Check available versions
composer show neuron-php/cms --all
In composer.json:
{
"require": {
"neuron-php/cms": "^1.0", // Major version constraint
"neuron-php/mvc": "^2.0"
}
}
# Update all neuron packages
composer update neuron-php/*
# Or update everything
composer update
# See what's blocking update
composer why-not neuron-php/cms 2.0
# Update with dependencies
composer update neuron-php/cms --with-all-dependencies
Symptom: Application breaks after update
Solutions:
# View package changelog
composer show neuron-php/cms --all | grep -A 20 "versions"
# Or check GitHub releases
# https://github.com/neuron-php/cms/releases
# Backup database
php scripts/backup-db.sh
# Backup files
tar -czf cms-backup-$(date +%Y%m%d).tar.gz /var/www/cms
# Don't jump major versions
# Upgrade 1.x -> 2.x -> 3.x
# Update to latest 1.x
composer require "neuron-php/cms:^1.0"
# Then update to 2.x
composer require "neuron-php/cms:^2.0"
./vendor/bin/neuron db:migrate
// Enable deprecation warnings
error_reporting( E_ALL | E_DEPRECATED );
In config/neuron.yaml:
app:
debug: true
environment: development
This enables:
Disable in production!
Log Locations:
logs/
├── application.log # General logs
├── error.log # Errors
├── access.log # HTTP access
└── worker.log # Background jobs
Viewing Logs:
# Last 100 lines
tail -n 100 logs/application.log
# Follow in real-time
tail -f logs/application.log
# Search for errors
grep -i error logs/application.log
# Search with context
grep -C 5 "CSRF" logs/application.log
# Filter by date
grep "2025-01-28" logs/application.log
Log Levels:
Enable Query Logging:
// In database connection
$db->setAttribute( PDO::ATTR_STATEMENT_CLASS, [LoggingStatement::class] );
// Or use MySQL general log
// my.cnf
[mysqld]
general_log = 1
general_log_file = /var/log/mysql/query.log
View Queries:
tail -f /var/log/mysql/query.log
Development:
ini_set( 'display_errors', 1 );
ini_set( 'display_startup_errors', 1 );
error_reporting( E_ALL );
Production:
ini_set( 'display_errors', 0 );
ini_set( 'log_errors', 1 );
ini_set( 'error_log', '/path/to/php-error.log' );
error_reporting( E_ALL & ~E_DEPRECATED & ~E_STRICT );
When reporting issues, provide:
# PHP version
php -v
# Installed extensions
php -m
# Composer version
composer --version
# Database version
mysql --version # or psql --version
# Neuron CMS version
composer show neuron-php/cms
# Configuration (sanitize sensitive data!)
cat config/neuron.yaml
# Recent logs
tail -n 50 logs/application.log
GitHub Issues: https://github.com/neuron-php/cms/issues
Include:
Example:
Title: CSRF validation fails on post creation form
Description:
When submitting the post creation form, I receive a "CSRF validation failed" error.
Steps to Reproduce:
1. Log in as admin
2. Navigate to /admin/posts/create
3. Fill out form
4. Click "Create Post"
5. Error appears
Expected: Post should be created
Actual: CSRF validation error
Environment:
- CMS Version: 1.2.0
- PHP: 8.4.1
- Database: MySQL 8.0
- OS: Ubuntu 22.04
Logs:
[2025-01-28 14:30:45] ERROR: CSRF validation failed in PostController.php:45
examples/ directory in packagesneuron-php