Extending the code
JournalPHP is extended with standard Laravel tools and new code that follows the project conventions.
Overview
New functionality is added with service providers, events, middleware and classes that follow the same conventions as the existing code: Actions, repositories with interfaces, services with swappable drivers. The conventions are described in Project architecture.
Service providers
Application providers are registered in config/app.php → providers:
| Provider | Purpose |
|---|---|
AppServiceProvider | General bootstrapping, including FORCE_HTTPS |
AuthServiceProvider | Authorization policies |
EventServiceProvider | Events, listeners, Socialite providers |
RouteServiceProvider | Route loading and rate limits |
BroadcastServiceProvider | WebSocket broadcast channels |
RepositoryServiceProvider | Automatic repository registration |
MetaBuilderServiceProvider | Page SEO builders |
MailServiceProvider | SMTP from admin panel settings |
MediaServiceProvider | Binds file storage via UploaderManager |
AiServiceProvider | Binds the AI service via AiManager |
ImageModerationServiceProvider | Binds image moderation via ImageModerationManager |
ScoutConfigServiceProvider | Search engine from admin panel settings |
HorizonServiceProvider, LogViewerServiceProvider | Access to Horizon and Log Viewer |
SlowQueryServiceProvider | Logging SQL queries slower than 700 ms |
Add your own bindings in AppServiceProvider::register() or in a separate provider registered in config/app.php:
$this->app->bind(MyServiceInterface::class, MyService::class);Actions
Business operations are implemented as Action classes with an execute() method:
namespace App\Actions\MyFeature;
class DoSomethingAction
{
public function __construct(
protected DoSomethingRepositoryInterface $repository,
) {}
public function execute(User $user, array $data): Result
{
// business logic
}
}Dependencies are injected through the constructor; the Action is injected as a controller method parameter:
public function store(MyRequest $request, DoSomethingAction $action): RedirectResponse
{
$action->execute($request->user(), $request->validated());
return back();
}Repositories
Create an interface in an Interfaces/ subdirectory and the implementation next to it:
app/Repositories/MyFeature/
├── Interfaces/
│ └── GetSomethingRepositoryInterface.php
└── GetSomethingRepository.phpRepositoryServiceProvider registers the binding automatically if the implementation is named like the interface without the Interface suffix.
New admin panel setting
Repository: in
app/Repositories/Config/MyFeature/, create a repository that reads the setting from theconfigtable. Return the default value from the repository or add it toConfigSeeder.Controller:
app/Http/Controllers/Admin/MyFeatureSettingsController.phpwitheditandupdatemethods. Save the value withConfig::updateOrCreate('myFeature', $value)— the model clears the cache itself.Routes: in
routes/admin.php:php$route->get('/settings/my-feature', [MyFeatureSettingsController::class, 'edit'])->name('settings.my-feature.edit'); $route->put('/settings/my-feature', [MyFeatureSettingsController::class, 'update'])->name('settings.my-feature.update');Section: in
config/admin_sections.php, add a key matching the path after/admin-panel/:php'settings/my-feature' => 'My setting',Page: an Inertia page in
resources/js/Admin/Pages/Settings/MyFeature/Edit.vue.Menu: an item in
resources/js/Admin/Layout/AppSidebar.vuewith the same path.
WARNING
A path without a key in config/admin_sections.php is accessible to any administrator. Add a key for every new section.
New OAuth provider
- Install the provider package from SocialiteProviders.
- Add its handler to
SocialiteWasCalledinEventServiceProvider::$listen. - Add the provider's
redirecttoconfig/services.php→oauth. - Add Client ID and Client Secret fields to
AuthSettingsController, a setting repository inapp/Repositories/Config/Auth/, and the form inresources/js/Admin/Pages/Settings/Auth/Edit.vue. - Add the provider to the list of public providers in
GetSharedInertiaConfigRepositoryso the button appears in the sign-in window.
New file storage
- Add a value and a name to
App\Enums\UploaderDriver. - Create a class in
app/Services/Uploader/Drivers/that extendsAbstractUploader. - Add a
create{Name}Driver()method toUploaderManager. - Add credential fields to
MediaSettingsControllerandresources/js/Admin/Pages/Settings/Media/Edit.vue.
New search engine
- Add a value to
App\Enums\SearchDriverand a translation of its name toresources/lang/*/enums.php. - Implement the driver in
app/Services/Search/Drivers/with theSearchDriverInterfaceandSearchIndexManagementInterfaceinterfaces, plus post and user search providers inapp/Services/Search/Providers/. - Register the driver with a
create{Name}Driver()method inSearchManager. - Pass the keys to the Scout configuration in
ScoutConfigServiceProvider. - Add fields to
UpdateSearchSettingsRequest,UpdateSearchSettingsActionandresources/js/Admin/Pages/Settings/Search/Edit.vue.
New GIF service
- Add a value and a name to
App\Enums\GifDriver. - Create a class in
app/Services/Gif/Drivers/that extendsAbstractGifServiceand implement theendpoint(),params(),results()andnext()methods. - Add a
create{Name}Driver()method toGifManager. - Add a branch with the service's keys to the
matchinAdmin/GifSettingsControllerand a section with fields inresources/js/Admin/Pages/Settings/Gifs/Edit.vue.
The driver returns results in the {results, next} format.
New AI provider
- Add a value to
App\Enums\AiProviderwith its name, default model and default API URL. - Add a
create{Name}Driver()method toAiManager. If the provider is compatible with the OpenAI Chat Completions API, the method returnsOpenAiCompatibleService. Otherwise, create a driver inapp/Services/Ai/Drivers/that extendsAbstractAiServiceand implementsrequest().
The provider appears in the list in Admin Panel → AI → Settings automatically.
New image moderation driver
- Add a value and a name to
App\Enums\ImageModerationDriver. - Create a class in
app/Services/Moderation/Image/Drivers/that extendsAbstractImageModerationServiceand implementsmoderateBatch(). - Add a
create{Name}Driver()method toImageModerationManager. - Add settings fields to
ImageModerationSettingsControllerandresources/js/Admin/Pages/AiIntegrations/ImageModerationSettings/Edit.vue.
New payment provider
- Add a value to
App\Enums\PaymentProviderDriverand a translated label toresources/lang/*/enums.php. - Implement
App\Services\Payments\Contracts\PaymentProviderInterfaceand an API client inapp/Services/Payments/Providers/{Name}/. - Add a
create{Name}Driver()method toPaymentManager. - Add the provider code to the
in:yookassa,tbankrule inSubscriptionPaymentProviderControllerandSubscriptionPaymentController, and key fields toSubscriptionPaymentProviderControllerand the settings form. - If the provider sends payment notifications, add a handler to
App\Http\Controllers\Payments\PaymentWebhookController.
New language
Translations live in resources/lang/:
| Files | Contents |
|---|---|
resources/lang/ru.json, resources/lang/en.json | Site interface strings |
resources/lang/ru/*.php, resources/lang/en/*.php | Server-side texts: authentication, validation, emails, errors (errors.php), messages (messages.php), enum value labels (enums.php), meta tags |
JSON files are loaded via laravel-vue-i18n. When adding a string, add it to all languages with the same key.
To add a language:
- Create
resources/lang/{code}.jsonwith interface strings. - Create the
resources/lang/{code}/directory and translate the files fromresources/lang/en/. - Import the Moment.js locale in
resources/js/app.jsfollowingmoment/dist/locale/ru. - Add the language code to the
in:en,ruvalidation rule inapp/Http/Controllers/Admin/GeneralSettingsController.php, to the language list on the general settings page (resources/js/Admin/Pages/Settings/General/Edit.vue) and to the list of allowed locales inapp/Http/Middleware/SetLocaleFromConfig.php.
Commands, events, middleware
php artisan make:command MyCommand
php artisan make:event SomethingHappened
php artisan make:listener HandleSomething --event=SomethingHappened
php artisan make:middleware MyMiddleware- Commands in
app/Console/Commands/are loaded automatically; the schedule is inapp/Console/Kernel.php. - Listeners are registered in
EventServiceProvider::$listen. - Middleware is registered in
app/Http/Kernel.php— globally, in a group or as an alias. - Model observers are attached with the
#[ObservedBy]attribute on the model.
Local packages
Code can be moved into a package and connected via a Composer path repository:
{
"repositories": [
{ "type": "path", "url": "./packages/my-package" }
]
}The package's service provider is registered automatically via extra.laravel.providers in its composer.json, or manually in config/app.php.
Forking
Updates from the upstream repository are pulled into a fork with these commands:
git remote add upstream https://github.com/ADamCarraway/journalphp.git
git fetch upstream
git merge upstream/master