Skip to content

salamonrafal/blog-system-ai

Repository files navigation

Blog System AI

Symfony foundation for a blog application with:

  • public article listing and details
  • admin area for managing content
  • Doctrine entity, repository, form and service layer
  • Twig templates and starter configuration
  • migration workflow for the database schema
  • authenticated admin access with role-based authorization

Development server

The project includes a small PHP-based development server helper.

Quick start:

  1. Install dependencies: composer install
  2. Create the SQLite database file: composer db:create
  3. Run migrations: composer db:migrate
  4. Start the development server: composer serve:debug:start
  5. Open the application: http://127.0.0.1:8888

JavaScript assets

JavaScript runs in two modes:

  • dev: Twig loads source modules directly from public/assets/js/ without minification
  • prod: Twig loads one bundled and minified file from public/assets/build/app.min.js

CSS assets

CSS also runs in two modes:

  • dev: Twig loads public/assets/css/styles.css, which imports all source files from public/assets/css/
  • prod: build generates one minified file in public/assets/build/styles.min.css

Current CSS split is thematic:

  • public/assets/css/base.css: variables, reset, layout base, global background
  • public/assets/css/components/: smaller shared UI parts such as topbar, navigation, mobile-menu, footer, back-to-top, privacy, buttons, forms, flash, admin-shortcuts, editor
  • public/assets/css/pages/: page-specific styles for home, admin, blog, error
  • public/assets/css/responsive.css: shared responsive and reduced-motion rules

public/assets/css/styles.css is the shared CSS manifest: in development Twig loads it directly, and in production it is used as the bundler entrypoint that imports the smaller source stylesheets in the correct order.

Build commands:

  • Install frontend dependencies: npm install
  • Reproducible install from lockfile: npm ci
  • Build production bundle with minification: npm run build:assets:prod
  • Short production build alias: npm run build:prod
  • Composer shortcut for production assets: composer assets:build:prod

In daily development Twig serves JavaScript modules from public/assets/js/ and the CSS manifest from public/assets/css/styles.css, so no frontend build step is required for regular work.

Optional command:

  • Build an unminified development bundle for manual inspection/debugging of the bundled output: npm run build:assets:dev

Useful commands:

  • Start server: composer serve:debug:start
  • Check status: composer serve:debug:status
  • Restart server: composer serve:debug:restart
  • Stop server: composer serve:debug:stop

The server writes its PID to:

Logs are written to:

Unit tests

The project includes PHPUnit-based unit tests for the domain and service layer:

How to run them:

  1. Install project dependencies: composer install
  2. Run the test suite: composer test

You can also run PHPUnit directly: vendor/bin/phpunit --configuration phpunit.xml.dist

The unit tests do not require a database. Repository-dependent logic uses PHPUnit mocks.

Release versioning

The project includes Composer shortcuts for semantic version bumps and Git tagging after finishing work.

Available commands:

  • Patch release: composer release:patch
  • Minor release: composer release:minor
  • Major release: composer release:major
  • Patch release with push: composer release:publish:patch
  • Minor release with push: composer release:publish:minor
  • Major release with push: composer release:publish:major

How it works:

  • the command reads the latest semantic Git tag
  • it calculates the next version based on patch, minor or major
  • it creates a new annotated Git tag on the current HEAD
  • it stops if the Git working tree is not clean
  • the release:publish:* variants also push the current branch and the new tag to origin

Supported tag formats:

  • v1.2.3
  • 1.2.3
  • prerelease tags such as 1.2.3-beta-1 or v1.2.3-rc.1

Notes:

  • if no semantic tag exists yet, the first generated tag starts from v0.0.1, v0.1.0 or v1.0.0
  • release:publish:* requires an existing origin remote and an active branch, not detached HEAD
  • release:* can also be used on detached HEAD; in that case it only creates the tag and suggests pushing just the tag manually
  • if you use release:* without publish, push branch and tag manually, for example: git push origin feature/my-branch v0.0.1

Recommended flow:

  1. Commit your changes: git add . && git commit -m "Describe changes"
  2. Create the tag and push branch with tag in one step: composer release:publish:patch

Suggested structure

.
|-- bin/
|-- config/
|   |-- packages/
|   |-- bootstrap.php
|   |-- bundles.php
|   `-- routes.yaml
|-- migrations/
|-- public/
|-- src/
|   |-- Command/
|   |-- Controller/
|   |   `-- Admin/
|   |-- Entity/
|   |-- Enum/
|   |-- Form/
|   |-- Repository/
|   |-- Security/
|   `-- Service/
|-- templates/
|   |-- admin/
|   |-- blog/
|   `-- security/
`-- var/

Database migrations

The project uses Doctrine Migrations. The schema is created by:

Typical workflow:

  1. Create the SQLite database file: composer db:create
  2. Run existing migrations: composer db:migrate
  3. After changing entities, generate a new migration: composer db:migration:diff
  4. Apply the new migration: composer db:migrate

For SQLite, the database file is configured in:

Current value: DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"

In this project composer db:create creates the SQLite file directly. It does not call doctrine:database:create, because that operation is not supported here by the SQLite platform/DBAL combination.

If you see no such table: article or no such table: user, it means migrations have not been executed yet.

Admin access

The admin area under /admin is protected by Symfony Security and requires ROLE_ADMIN.

Setup flow:

  1. Install packages: composer install
  2. Create the SQLite file: composer db:create
  3. Run migrations: composer db:migrate
  4. Create the first administrator: composer user:create-admin
  5. Open the login page: /login

The create-admin command also accepts arguments: php bin/console app:user:create-admin admin@example.com strong-password

Main security files:

Media library

The admin panel includes a media module for uploading and browsing blog images.

Main views:

  • upload form: /admin/media
  • gallery: /admin/media/gallery

How media storage works:

  • uploaded image metadata is stored in the media_image database table
  • files are written to dated subdirectories inside public/uploads/media/
  • the stored file_path in the database is treated as the source of truth for files managed by the gallery

Example storage layout:

  • public/uploads/media/2026/04/05/...

Orphaned media files

An orphaned media file is a file that still exists on disk in public/uploads/media/, but no longer has a matching row in the media_image table.

This can happen for example when:

  • files were copied manually to the media directory
  • database content was restored or changed without restoring matching files
  • old development data left files on disk after database resets

To help clean this up, the project includes a console command:

php bin/console app:media:archive-orphans

What the command does:

  • scans public/uploads/media/ recursively
  • compares discovered files with media_image.file_path
  • moves orphaned files to a temporary staging directory inside var/
  • creates a ZIP archive with the moved files
  • prints the moved file list and generated archive path in the console output

Folders used by the command:

Ignored filenames:

  • by default the command ignores .gitkeep
  • the ignore list is configurable in config/services.yaml under app.media_orphan_ignored_filenames
  • add more filenames there if your media directory contains placeholder or maintenance files that should never be archived

Archive naming:

  • var/media-orphans/media-orphans-YYYYMMDD-HHMMSS-<random>.zip

Notes:

  • tracked files that have a matching media_image.file_path entry are left untouched
  • files whose basename is present in app.media_orphan_ignored_filenames are skipped even if they are not present in the database
  • if no orphaned files are found, the command exits successfully and reports that nothing had to be archived
  • empty temporary directories created during the process are cleaned up automatically
  • the command does not modify database rows; it only moves untracked files from disk and archives them
  • because the archive is written under var/, it is treated as runtime/generated data rather than user-facing media

Export queues

The project includes background export queues for:

  • articles
  • article keywords
  • article categories
  • top menu

All generated files are stored in: var/exports/

The admin panel includes:

  • queue view: /admin/queues/status
  • export history and download list: /admin/exports

Article export

Flow overview:

  • In the admin article list, exporting an article creates a pending record in article_export_queue
  • The console consumer collects pending queue items and builds a JSON export file with article metadata and content
  • After processing, the consumer creates a record in article_export with status, type and file path
  • The exported article payload includes stable identifiers such as:
    • article slug
    • assigned category category_slug

Important files:

Manual run:

  • Composer shortcut: composer article-export:process-queue
  • Direct Symfony command: php bin/console app:article-export:process-queue

What the manual run does:

  • reads pending entries from article_export_queue
  • sets them to processing
  • generates a JSON export file in var/exports
  • creates a new record in article_export
  • marks processed queue entries as completed

If there are no pending entries, the command exits successfully and prints: No queued article exports to process.

Category export

Flow overview:

  • In the admin category list, exporting a category creates a pending record in category_export_queue
  • The console consumer builds a JSON export file with category metadata and translations
  • The exported category payload includes a stable category slug

Important files:

Manual run:

  • Composer shortcut: composer category-export:process-queue
  • Direct Symfony command: php bin/console app:category-export:process-queue

Article keyword export

Flow overview:

  • In the admin article keyword view, exporting creates a pending record in article_keyword_export_queue
  • The console consumer exports the full keyword dictionary to one JSON file
  • The exported keyword payload includes stable identifiers and metadata such as:
    • keyword name
    • keyword language
    • assigned article_ids

Important files:

Manual run:

  • Composer shortcut: composer article-keyword-export:process-queue
  • Direct Symfony command: php bin/console app:article-keyword-export:process-queue

Top menu export

Flow overview:

  • In the admin top menu view, exporting creates a pending record in top_menu_export_queue
  • The console consumer exports the full menu hierarchy to one JSON file
  • The exported menu item payload includes stable identifiers for cross-environment matching:
    • item unique_name
    • parent parent_unique_name
    • category target category_slug
    • article target article_slug

Important files:

Manual run:

  • Composer shortcut: composer top-menu-export:process-queue
  • Direct Symfony command: php bin/console app:top-menu-export:process-queue

Scheduled processing with cron

The repository includes a ready cron file:

Current entries process all background queues once per minute:

  • app:article-export:process-queue
  • app:category-export:process-queue
  • app:article-keyword-export:process-queue
  • app:top-menu-export:process-queue
  • app:article-import:process-queue
  • app:article-keyword-import:process-queue
  • app:category-import:process-queue
  • app:top-menu-import:process-queue

Before running consumers manually or from cron, make sure the database exists and migrations are applied:

  1. Install dependencies: composer install
  2. Create the SQLite database file: composer db:create
  3. Run migrations: composer db:migrate

Import queue

The project also includes an article import queue for processing previously exported JSON files in the background.

Flow overview:

  • In the admin panel, uploading a file on /admin/imports creates a pending record in article_import_queue
  • The console consumer reads the queued JSON export file and expects the article-export format produced by the export mechanism
  • If an article with the same slug already exists, it is updated
  • If no article with that slug exists, a new article is created
  • If validation fails or the file is invalid, the queue item is marked as failed and the error reason is stored in error_message
  • Uploaded files are stored in: var/imports/
  • The admin panel includes:
    • upload and status list: /admin/imports
    • pending queue view: /admin/queues/status

Important files:

Manual consumer run

Before running the import consumer manually, make sure the database exists and migrations are applied:

  1. Install dependencies: composer install
  2. Create the SQLite database file: composer db:create
  3. Run migrations: composer db:migrate

Run the queue consumer manually with one of these commands:

  • Composer shortcut: composer article-import:process-queue
  • Direct Symfony command: php bin/console app:article-import:process-queue

What the manual run does:

  • reads pending entries from article_import_queue
  • sets them to processing
  • loads the uploaded JSON file from var/imports
  • validates required fields and article constraints
  • updates an existing article by slug or creates a new one
  • marks successful items as completed
  • marks invalid items as failed and stores the reason in error_message

If there are no pending entries, the command exits successfully and prints: No queued article imports to process.

Category import queue

The project also includes a dedicated category import queue for restoring category data from exported JSON files in the background.

Flow overview:

  • In the admin panel, uploading a file on /admin/category-imports creates a pending record in category_import_queue
  • The console consumer reads the category-export JSON format produced by the category export mechanism
  • Export payloads store category entries under the categories array key
  • If a category with the same slug already exists, it is updated
  • If no category with that slug exists, a new category is created
  • If validation fails or the file is invalid, the queue item is marked as failed and the error reason is stored in error_message

Manual run:

  • Composer shortcut: composer category-import:process-queue
  • Direct Symfony command: php bin/console app:category-import:process-queue

Important files:

Article keyword import queue

The project also includes a dedicated article keyword import queue for restoring keyword data from exported JSON files.

Flow overview:

  • In the admin panel, uploading a file on /admin/article-keyword-imports creates a pending record in article_keyword_import_queue
  • The console consumer reads the article-keyword-export JSON format
  • Existing keywords are matched by the pair language + name
  • If a keyword with the same language + name already exists, it is updated
  • If no keyword with that language + name exists, a new keyword is created
  • Keyword-to-article assignments from the import file are ignored, so current article links stay untouched
  • If validation fails or the file is invalid, the queue item is marked as failed and the error reason is stored in error_message

Manual run:

  • Composer shortcut: composer article-keyword-import:process-queue
  • Direct Symfony command: php bin/console app:article-keyword-import:process-queue

Important files:

Top menu import queue

The project also includes a dedicated top menu import queue for restoring the menu hierarchy from exported JSON files.

Flow overview:

  • In the admin panel, uploading a file on /admin/top-menu/imports creates a pending record in top_menu_import_queue
  • The console consumer reads the top-menu-export JSON format
  • Existing items are matched by unique_name
  • Parent relations are resolved by parent_unique_name
  • Items are processed from the highest hierarchy level to the lowest so parent items are available before their children
  • If validation fails or the file is invalid, the queue item is marked as failed and the error reason is stored in error_message

Manual run:

  • Composer shortcut: composer top-menu-import:process-queue
  • Direct Symfony command: php bin/console app:top-menu-import:process-queue

For a local non-Docker setup you can use equivalent crontab entries, for example:

  • * * * * * cd /path/to/project && composer article-export:process-queue
  • * * * * * cd /path/to/project && composer category-export:process-queue
  • * * * * * cd /path/to/project && composer article-keyword-export:process-queue
  • * * * * * cd /path/to/project && composer top-menu-export:process-queue
  • * * * * * cd /path/to/project && composer article-import:process-queue
  • * * * * * cd /path/to/project && composer article-keyword-import:process-queue
  • * * * * * cd /path/to/project && composer category-import:process-queue
  • * * * * * cd /path/to/project && composer top-menu-import:process-queue

Next steps

  1. Add password reset and email verification.
  2. Split admin roles into editor and administrator.
  3. Add audit logging for content changes.
  4. Extend the domain with categories, tags, comments and media.

Docker Manual Command

Persistent data in Docker

If you recreate the container and want to keep application data, mount the directories that store runtime files and uploads outside the container.

Required mounts:

  • directory containing the SQLite *.db file - mount the folder where the database file configured in DATABASE_URL is stored
  • public/uploads/media/ - keeps uploaded media library files
  • public/uploads/avatars/ - keeps uploaded user avatars

Optional mounts, if you also want to keep generated or uploaded helper files:

  • var/imports/ - queued import files uploaded in the admin panel
  • var/exports/ - generated export files
  • var/media-orphans/ - ZIP archives created by app:media:archive-orphans

Example with bind mounts:

docker container run -d -p 8888:8888 -p 8080:80 \
   -e APP_ENV=dev \
   -e APP_DEBUG=1 \
   -e APP_SECRET="test12345_deko1" \
   -e DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" \
   -v "$(pwd)/var:/var/www/app/var" \
   -v "$(pwd)/public/uploads/media:/var/www/app/public/uploads/media" \
   -v "$(pwd)/public/uploads/avatars:/var/www/app/public/uploads/avatars" \
   --name blog-system-ai salamonrafal/blog-system-ai:dev

If you use SQLite, make sure the mounted directory matches the current DATABASE_URL setting and points to the folder that contains the *.db file. In the default configuration this is var/, because the database file is stored as var/data.db. The upload directories should still be mounted separately so media files and avatars are not lost.

Build image

docker image build -t salamonrafal/blog-system-ai:dev .

Create develop container

docker container run -d -p 8888:8888 -p 8080:80 \
   -e APP_ENV=dev \
   -e APP_DEBUG=1 \
   -e APP_SECRET="test12345_deko1" \
   -e DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" \
 --name blog-system-ai salamonrafal/blog-system-ai:dev

Run command in container

docker container exec -it blog-system-ai bash

Run Composer install

docker container exec -u www-data -it blog-system-ai composer install

Run database migrations

docker container exec -u www-data -it blog-system-ai php bin/console doctrine:migrations:migrate --no-interaction

Display log

docker container logs blog-system-ai

Delete develop container

docker container stop blog-system-ai && docker container rm blog-system-ai

About

AI-powered blog system built on Symfony. Features public article listings, an admin panel for content management, Doctrine-based architecture (entities, repositories, forms, services), Twig templates, database migrations, and role-based authentication. Developed primarily through AI-assisted pair programming.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors