Skip to content

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:

ProviderPurpose
AppServiceProviderGeneral bootstrapping, including FORCE_HTTPS
AuthServiceProviderAuthorization policies
EventServiceProviderEvents, listeners, Socialite providers
RouteServiceProviderRoute loading and rate limits
BroadcastServiceProviderWebSocket broadcast channels
RepositoryServiceProviderAutomatic repository registration
MetaBuilderServiceProviderPage SEO builders
MailServiceProviderSMTP from admin panel settings
MediaServiceProviderBinds file storage via UploaderManager
AiServiceProviderBinds the AI service via AiManager
ImageModerationServiceProviderBinds image moderation via ImageModerationManager
ScoutConfigServiceProviderSearch engine from admin panel settings
HorizonServiceProvider, LogViewerServiceProviderAccess to Horizon and Log Viewer
SlowQueryServiceProviderLogging SQL queries slower than 700 ms

Add your own bindings in AppServiceProvider::register() or in a separate provider registered in config/app.php:

php
$this->app->bind(MyServiceInterface::class, MyService::class);

Actions ​

Business operations are implemented as Action classes with an execute() method:

php
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:

php
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.php

RepositoryServiceProvider registers the binding automatically if the implementation is named like the interface without the Interface suffix.

New admin panel setting ​

  1. Repository: in app/Repositories/Config/MyFeature/, create a repository that reads the setting from the config table. Return the default value from the repository or add it to ConfigSeeder.

  2. Controller: app/Http/Controllers/Admin/MyFeatureSettingsController.php with edit and update methods. Save the value with Config::updateOrCreate('myFeature', $value) — the model clears the cache itself.

  3. 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');
  4. Section: in config/admin_sections.php, add a key matching the path after /admin-panel/:

    php
    'settings/my-feature' => 'My setting',
  5. Page: an Inertia page in resources/js/Admin/Pages/Settings/MyFeature/Edit.vue.

  6. Menu: an item in resources/js/Admin/Layout/AppSidebar.vue with 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 ​

  1. Install the provider package from SocialiteProviders.
  2. Add its handler to SocialiteWasCalled in EventServiceProvider::$listen.
  3. Add the provider's redirect to config/services.php → oauth.
  4. Add Client ID and Client Secret fields to AuthSettingsController, a setting repository in app/Repositories/Config/Auth/, and the form in resources/js/Admin/Pages/Settings/Auth/Edit.vue.
  5. Add the provider to the list of public providers in GetSharedInertiaConfigRepository so the button appears in the sign-in window.

New file storage ​

  1. Add a value and a name to App\Enums\UploaderDriver.
  2. Create a class in app/Services/Uploader/Drivers/ that extends AbstractUploader.
  3. Add a create{Name}Driver() method to UploaderManager.
  4. Add credential fields to MediaSettingsController and resources/js/Admin/Pages/Settings/Media/Edit.vue.
  1. Add a value to App\Enums\SearchDriver and a translation of its name to resources/lang/*/enums.php.
  2. Implement the driver in app/Services/Search/Drivers/ with the SearchDriverInterface and SearchIndexManagementInterface interfaces, plus post and user search providers in app/Services/Search/Providers/.
  3. Register the driver with a create{Name}Driver() method in SearchManager.
  4. Pass the keys to the Scout configuration in ScoutConfigServiceProvider.
  5. Add fields to UpdateSearchSettingsRequest, UpdateSearchSettingsAction and resources/js/Admin/Pages/Settings/Search/Edit.vue.

New GIF service ​

  1. Add a value and a name to App\Enums\GifDriver.
  2. Create a class in app/Services/Gif/Drivers/ that extends AbstractGifService and implement the endpoint(), params(), results() and next() methods.
  3. Add a create{Name}Driver() method to GifManager.
  4. Add a branch with the service's keys to the match in Admin/GifSettingsController and a section with fields in resources/js/Admin/Pages/Settings/Gifs/Edit.vue.

The driver returns results in the {results, next} format.

New AI provider ​

  1. Add a value to App\Enums\AiProvider with its name, default model and default API URL.
  2. Add a create{Name}Driver() method to AiManager. If the provider is compatible with the OpenAI Chat Completions API, the method returns OpenAiCompatibleService. Otherwise, create a driver in app/Services/Ai/Drivers/ that extends AbstractAiService and implements request().

The provider appears in the list in Admin Panel → AI → Settings automatically.

New image moderation driver ​

  1. Add a value and a name to App\Enums\ImageModerationDriver.
  2. Create a class in app/Services/Moderation/Image/Drivers/ that extends AbstractImageModerationService and implements moderateBatch().
  3. Add a create{Name}Driver() method to ImageModerationManager.
  4. Add settings fields to ImageModerationSettingsController and resources/js/Admin/Pages/AiIntegrations/ImageModerationSettings/Edit.vue.

New payment provider ​

  1. Add a value to App\Enums\PaymentProviderDriver and a translated label to resources/lang/*/enums.php.
  2. Implement App\Services\Payments\Contracts\PaymentProviderInterface and an API client in app/Services/Payments/Providers/{Name}/.
  3. Add a create{Name}Driver() method to PaymentManager.
  4. Add the provider code to the in:yookassa,tbank rule in SubscriptionPaymentProviderController and SubscriptionPaymentController, and key fields to SubscriptionPaymentProviderController and the settings form.
  5. If the provider sends payment notifications, add a handler to App\Http\Controllers\Payments\PaymentWebhookController.

New language ​

Translations live in resources/lang/:

FilesContents
resources/lang/ru.json, resources/lang/en.jsonSite interface strings
resources/lang/ru/*.php, resources/lang/en/*.phpServer-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:

  1. Create resources/lang/{code}.json with interface strings.
  2. Create the resources/lang/{code}/ directory and translate the files from resources/lang/en/.
  3. Import the Moment.js locale in resources/js/app.js following moment/dist/locale/ru.
  4. Add the language code to the in:en,ru validation rule in app/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 in app/Http/Middleware/SetLocaleFromConfig.php.

Commands, events, middleware ​

bash
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 in app/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:

json
{
    "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:

bash
git remote add upstream https://github.com/ADamCarraway/journalphp.git
git fetch upstream
git merge upstream/master

Released under the MIT License.