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
| Layer | Technologies |
|---|---|
| Backend | PHP, Laravel |
| Frontend | Vue 3, Inertia.js, Vite, Tailwind CSS, Element Plus |
| Database | MySQL |
| Cache, sessions, queues | Redis, Laravel Horizon |
| WebSocket | Laravel Reverb, Laravel Echo |
| Search | Laravel 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 JSONSite 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
| Directory | Purpose |
|---|---|
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 pointThe 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.phpRepositoryServiceProvider 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/:
| Interface | Trait | Capability |
|---|---|---|
BookmarkableInterface | Bookmarkable | Bookmarking |
ReactionableInterface | Reactionable | Reactions |
ReportableInterface | Reportable | Reports |
IgnorableInterface | Ignorable | Hiding by a user |
NotifiableInterface | Notifiable | Notification subscription |
FollowableInterface | HasFollowers | Having followers |
FollowerInterface | Followable | Following users and topics |
BannableInterface | Bannable | Bans |
HasNotificationsInterface | HasNotifications | A user's notification subscriptions |
HasMedia | — | Media attachments |
| — | HasBookmarks, HasIgnores | A 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
| Directory | Purpose |
|---|---|
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
| Integration | Manager | Enum | Drivers |
|---|---|---|---|
| AI | AiManager | AiProvider | DeepSeek, OpenAI, Gemini, YandexGPT |
| Search | SearchManager | SearchDriver | Database, Algolia, Meilisearch |
| GIF search | GifManager | GifDriver | Tenor, Giphy |
| File storage | UploaderManager | UploaderDriver | Local, Cloudinary, Yandex Cloud |
| Image moderation | ImageModerationManager | ImageModerationDriver | Yandex Vision, no check |
| Payments | PaymentManager | PaymentProviderDriver | YooKassa, 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/:
| File | Contents |
|---|---|
algolia-posts.php, algolia-users.php | Search fields, ranking and filtering for Algolia |
meilisearch-posts.php, meilisearch-users.php | Search 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
| Model | Purpose |
|---|---|
User | Users and topics: a topic is a User with type = community |
Post | Posts: blocks in JSON, category_id is the topic, reply_id is the original post of a reply post |
PostVersionHistory | Post versions |
Comment | Threaded comments |
Tag, Tagable | Tags and their links |
Reaction, ReactionItem | Reactions and the reaction set |
Achievement, UserAchievement | Achievements and their awarding to users |
MonthlyRatingPlace | Final places in monthly tops |
Chat, Message | Private messages |
Notification | Users' notification subscriptions (notification_subscriptions table) |
Ban, ContentBan, ViolationBanRule | Bans, publishing restrictions and automatic restriction rules |
Complaint, ComplaintReason | Reports and their reasons |
SubscriptionPrice, SubscriptionFeature, UserSubscription, SubscriptionPayment | Subscription |
Advertisement | Ad blocks |
Media | Uploaded files |
Config | Platform settings |
Role, UserRole, UserAdminSection | Roles and access to admin panel sections |
Database
- Users are soft-deleted; final account deletion is deferred via the
will_be_deleted_atfield - Post counters are stored in the
post_countstable and recalculated withphp 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