Skip to content

Project architecture ​

JournalPHP is a Laravel monolith with a Vue 3 frontend connected to the server via Inertia.js.

Overview ​

The server renders pages through Inertia: a controller returns the name of a Vue page and its data, and navigation between pages happens without a full reload. Business logic lives outside controllers in Actions, data access in repositories with interfaces, and integrations in services with swappable drivers. Platform settings are stored in the database, so integration drivers and keys are chosen in the admin panel without editing .env.

Background work runs in the same application: Laravel Horizon processes queues, Laravel Reverb delivers WebSocket messages, and the Laravel scheduler runs scheduled tasks. Component versions are listed in System requirements.

Stack ​

LayerTechnologies
BackendPHP, Laravel
FrontendVue 3, Inertia.js, Vite, Tailwind CSS, Element Plus
DatabaseMySQL
Cache, sessions, queuesRedis, Laravel Horizon
WebSocketLaravel Reverb, Laravel Echo
SearchLaravel Scout: database, Algolia, Meilisearch

Request flow ​

HTTP request
  → Middleware (auth, admin.access, content-ban, has.subscription.feature, …)
    → Controller + Form Request (validation)
      → Action (one business operation)
        → Service / Repository
          → Inertia response (Vue page and its data) or JSON

Site routes are in routes/web.php; admin panel routes with the admin-panel prefix are in routes/admin.php, which is included from web.php. WebSocket channels are in routes/channels.php. The task schedule is in app/Console/Kernel.php.

app/ structure ​

DirectoryPurpose
Http/Controllers/Controllers: accept a request and delegate the work
Http/Middleware/Middleware
Http/Requests/Form Requests — input validation
Http/Resources/API Resources — response shaping
Actions/One business operation per class
Services/Domain logic and integrations
Repositories/Data access through interfaces
DTO/Typed data transfer objects
Contracts/, Concerns/Interfaces for shared model capabilities and the traits that implement them
Enums/PHP enums
Models/Eloquent models
Events/, Listeners/Domain events and their handlers
Jobs/Background jobs
Notifications/Laravel notifications
Observers/Model lifecycle hooks
Policies/Authorization rules
Rules/Custom validation rules
Helpers/Global functions for checking subscription features
Console/Commands/Artisan commands
Providers/Service providers

Frontend ​

resources/js/
├── Pages/        # Inertia pages of the site
├── components/   # Site components
├── editor/       # Post editor modules
├── Shared/       # Shared layout and global modals
├── Admin/        # Admin panel pages and components
├── app.js        # Site entry point
└── admin.js      # Admin panel entry point

The main styles are resources/css/app.css, theme CSS variables are resources/css/variables.css, and admin panel styles are resources/css/admin.css. Tailwind is configured in tailwind.config.cjs. Default colors for Admin Panel → Settings → Site → Appearance are defined in config/appearance.php.

Actions ​

An Action is a class with a single public execute() method that performs one operation. Actions are grouped by domain: app/Actions/Post/StorePostAction.php, app/Actions/Comment/StoreCommentAction.php, app/Actions/Search/ImportSearchIndexesAction.php. They are called from controllers, listeners, jobs and other Actions.

Repositories ​

A repository consists of an interface in the Interfaces/ subdirectory and an implementation next to it:

app/Repositories/Post/
├── Interfaces/
│   └── GetPostBySlugRepositoryInterface.php
└── GetPostBySlugRepository.php

RepositoryServiceProvider finds these pairs and automatically registers the "interface → implementation" binding if the implementation is named like the interface without the Interface suffix.

Repositories in app/Repositories/Config/ read and write settings from the config table. When a record changes, the Config model clears the related caches.

DTOs ​

DTOs in app/DTO/ pass data between layers instead of untyped arrays, for example CreatePostDto when a post is created.

Contracts and traits ​

Shared model capabilities are described by interfaces in app/Contracts/ and implemented by traits from app/Concerns/:

InterfaceTraitCapability
BookmarkableInterfaceBookmarkableBookmarking
ReactionableInterfaceReactionableReactions
ReportableInterfaceReportableReports
IgnorableInterfaceIgnorableHiding by a user
NotifiableInterfaceNotifiableNotification subscription
FollowableInterfaceHasFollowersHaving followers
FollowerInterfaceFollowableFollowing users and topics
BannableInterfaceBannableBans
HasNotificationsInterfaceHasNotificationsA user's notification subscriptions
HasMedia—Media attachments
—HasBookmarks, HasIgnoresA user's bookmarks and hidden items

Feeds ​

A feed (app/Services/Feed/Feed.php) is built with Laravel Pipeline:

Feed
  → MyFeed           — posts from subscriptions
  → NewFeed          — recent posts
  → PopularFeed      — period filter
  → EditorialFeed    — editorial posts
  → OrderFeed        — sorting
  → WithoutBlackList — excludes hidden users, posts and keywords
→ cursorPaginate(10)

MyFeed, NewFeed and PopularFeed apply only to their own feed type, EditorialFeed applies with the editorial filter. OrderFeed picks the sort order by feed type, and WithoutBlackList applies to every feed of a signed-in user.

Services ​

DirectoryPurpose
Feed/Building feeds
Search/Search drivers: database, Algolia, Meilisearch
Payments/Payment providers: YooKassa, T-Bank
Subscription/Subscription payments and auto-renewal
Ai/AI providers and the comment bot
Moderation/AI moderation of posts, comments and images
Uploader/File storage: local disk, Cloudinary, Yandex Cloud
Gif/GIF search: Tenor, Giphy
MetaBuilder/Meta tags, OpenGraph and JSON-LD for pages
Seo/Reading metadata of external links
Post/Post blocks, polls, summaries, counters
Comment/Shaping responses with comments
User/A user's badge and featured achievement
Auth/OAuth and One Tap token verification
Advertisement/Selecting and rendering ad blocks
SharedData/Shared data for all Inertia pages
Admin/Admin panel sections

Swappable drivers ​

Integrations chosen in the admin panel share the same structure:

app/Services/Gif/
├── Contracts/GifServiceInterface.php   — contract
├── AbstractGifService.php              — shared driver algorithm
├── Drivers/                            — implementations
│   ├── TenorGifService.php
│   └── GiphyGifService.php
└── GifManager.php                      — driver selection and creation
  • Contract: describes what any driver can do
  • Abstract class: holds the shared part, a driver implements only the differences
  • Manager: extends Illuminate\Support\Manager, getDefaultDriver() takes the driver from settings, create{Name}Driver() methods create drivers
  • Enum: lists the drivers and their names for the admin panel
IntegrationManagerEnumDrivers
AIAiManagerAiProviderDeepSeek, OpenAI, Gemini, YandexGPT
SearchSearchManagerSearchDriverDatabase, Algolia, Meilisearch
GIF searchGifManagerGifDriverTenor, Giphy
File storageUploaderManagerUploaderDriverLocal, Cloudinary, Yandex Cloud
Image moderationImageModerationManagerImageModerationDriverYandex Vision, no check
PaymentsPaymentManagerPaymentProviderDriverYooKassa, T-Bank

DeepSeek, Gemini and YandexGPT use the shared OpenAiCompatibleService driver: they are compatible with the OpenAI Chat Completions API. OpenAiService extends it and differs in request parameters.

The AI, storage and image moderation contracts are registered in service providers as scoped: an instance is created anew for each request and each queue job, so workers immediately use the driver chosen in the admin panel.

A search driver implements two interfaces: SearchDriverInterface (post and user search providers) and SearchIndexManagementInterface (connection check and index import). ScoutConfigServiceProvider passes the driver and keys from the admin panel to Scout when the application boots.

Mail works differently: MailServiceProvider puts the SMTP settings from the admin panel into the mail configuration when the application boots, and Laravel's built-in MailManager chooses the transport. After the mail settings are saved, MailSettingsController runs queue:restart, and the queue workers restart with the new settings, see Email.

Search indexes ​

Index parameters are described in config/scout-indexes/:

FileContents
algolia-posts.php, algolia-users.phpSearch fields, ranking and filtering for Algolia
meilisearch-posts.php, meilisearch-users.phpSearch fields, filters, sorting, ranking rules and typo tolerance for Meilisearch

In meilisearch-posts.php, the rankingScoreThreshold parameter (0.5 by default) cuts off weak matches. After the files change, the index is synchronized again — see Search.

Models ​

ModelPurpose
UserUsers and topics: a topic is a User with type = community
PostPosts: blocks in JSON, category_id is the topic, reply_id is the original post of a reply post
PostVersionHistoryPost versions
CommentThreaded comments
Tag, TagableTags and their links
Reaction, ReactionItemReactions and the reaction set
Achievement, UserAchievementAchievements and their awarding to users
MonthlyRatingPlaceFinal places in monthly tops
Chat, MessagePrivate messages
NotificationUsers' notification subscriptions (notification_subscriptions table)
Ban, ContentBan, ViolationBanRuleBans, publishing restrictions and automatic restriction rules
Complaint, ComplaintReasonReports and their reasons
SubscriptionPrice, SubscriptionFeature, UserSubscription, SubscriptionPaymentSubscription
AdvertisementAd blocks
MediaUploaded files
ConfigPlatform settings
Role, UserRole, UserAdminSectionRoles and access to admin panel sections

Database ​

  • Users are soft-deleted; final account deletion is deferred via the will_be_deleted_at field
  • Post counters are stored in the post_counts table and recalculated with php artisan post-counts:refresh
  • Tags, reactions, bookmarks, reports and media are attached through polymorphic relations

Infrastructure ​

docker-compose.yml          — local environment
docker-compose.prod.yml     — production
Dockerfile                  — application image for development
Dockerfile.prod             — multi-stage build: assets → PHP runtime → nginx
docker/entrypoint-*.sh      — container startup scripts
docker/nginx/               — nginx configurations
supervisord.conf            — PHP-FPM, Horizon, scheduler, Reverb

Released under the MIT License.