Home

Awesome

Laravel Cached Database Stickiness<br>Build Status Coverage Status Scrutinizer Code Quality

Guarantee database stickiness over the same user's consecutive requests.

Requirements

Installing

composer require mpyw/laravel-cached-database-stickiness

[!IMPORTANT] The default implementation is provided by ConnectionServiceProvider, however, package discovery is not available. Be careful that you MUST register it in config/app.php by yourself.

<?php

return [

    /* ... */

    'providers' => [

        /* ... */

        Mpyw\LaravelCachedDatabaseStickiness\ConnectionServiceProvider::class,

        /* ... */

    ],

    /* ... */
];

Then select the proper cache driver:

DriverIs eligible?Description
redis😄Very fast, scalable and reliable driver<br>(Cluster mode must be disabled)
memcached😄Alternative for Redis
dynamodb😧It works but not so suitable for short-term caching<br>(ConsistentRead must be enabled)
apc😧It works unless PHP processes are running in multiple machines or containers
file😧It works unless PHP processes are running in multiple machines or containers
<del>database</del>🤮We'll get into a chicken-or-egg situation
<del>array</del>🤮Just for testing

Features

This library provides the following features.

Diagrams

Default

default

Sticky

sticky

Sticky Cached

sticky-cached

Advanced Usage

Customize Stickiness TTL

[!NOTE] The default stickiness TTL is 5 seconds.
You can configure this value to add stickiness_ttl directive to your config/database.php.

<?php

return [

    /* ... */

    'default' => env('DB_CONNECTION', 'mysql'),

    /*
    |--------------------------------------------------------------------------
    | Database Connections
    |--------------------------------------------------------------------------
    |
    | Here are each of the database connections setup for your application.
    | Of course, examples of configuring each database platform that is
    | supported by Laravel is shown below to make development simple.
    |
    |
    | All database work in Laravel is done through the PHP PDO facilities
    | so make sure you have the driver for your particular database of
    | choice installed on your machine before you begin development.
    |
    */

    'connections' => [

        /* ... */

        'mysql' => [
            'read' => env('DB_HOST_READONLY') ? [
                'host' => env('DB_HOST_READONLY'),
            ] : null,
            'write' => [],
            'sticky' => (bool)env('DB_HOST_READONLY'),
            'stickiness_ttl' => 3, // Set the stickiness TTL to 3 seconds
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),

            /* ... */
        ],

    ],
    
];

Customize Connection Implementation

[!TIP] You can configure Connection implementation.

<?php

namespace App\Providers;

use App\Database\MySqlConnection;
use Illuminate\Database\Connection;
use Illuminate\Support\ServiceProvider;

class DatabaseServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        Connection::resolverFor('mysql', function (...$parameters) {
            return new MySqlConnection(...$parameters);
        });
    }
}
<?php

namespace App\Database;

use Illuminate\Database\Connection as BaseMySqlConnection;
use Mpyw\LaravelCachedDatabaseStickiness\DispatchesConnectionEvents;

class MySqlConnection extends BaseMySqlConnection
{
    use DispatchesConnectionEvents;
}

Customize Stickiness Source

[!TIP] You can register the StickinessResolverInterface implementation to change the source for stickiness determination.

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Mpyw\LaravelCachedDatabaseStickiness\StickinessResolvers\AuthBasedResolver;
use Mpyw\LaravelCachedDatabaseStickiness\StickinessResolvers\StickinessResolverInterface;

class DatabaseServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(StickinessResolverInterface::class, AuthBasedResolver::class);
    }
}
SourceMiddleware
IpBasedResolver<br>(Default)Remote IP address
AuthBasedResolverAuthenticated User IDRequired

[!IMPORTANT] You must add ResolveStickinessOnResolvedConnections middleware after Authenticate when you use AuthBasedResolver.

--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
 <?php
 
 namespace App\Http;
 
 use Illuminate\Foundation\Http\Kernel as HttpKernel;
 
 class Kernel extends HttpKernel
 {
     /* ... */
 
     /**
      * The application's route middleware groups.
      *
      * @var array
      */
     protected $middlewareGroups = [
         'web' => [
             \App\Http\Middleware\EncryptCookies::class,
             \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
             \Illuminate\Session\Middleware\StartSession::class,
             // \Illuminate\Session\Middleware\AuthenticateSession::class,
             \Illuminate\View\Middleware\ShareErrorsFromSession::class,
             \App\Http\Middleware\VerifyCsrfToken::class,
             \Illuminate\Routing\Middleware\SubstituteBindings::class,
         ],
 
         'api' => [
             'throttle:60,1',
             \Illuminate\Routing\Middleware\SubstituteBindings::class,
         ],
+
+        'auth' => [
+            \App\Http\Middleware\Authenticate::class,
+            \Mpyw\LaravelCachedDatabaseStickiness\Http\Middleware\ResolveStickinessOnResolvedConnections::class,
+        ],
+
+        'auth.basic' => [
+            \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
+            \Mpyw\LaravelCachedDatabaseStickiness\Http\Middleware\ResolveStickinessOnResolvedConnections::class,
+        ],
     ];
 
     /**
      * The application's route middleware.
      *
      * These middleware may be assigned to groups or used individually.
      *
      * @var array
      */
     protected $routeMiddleware = [
-        'auth' => \App\Http\Middleware\Authenticate::class,
-        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
         'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
         'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
         'can' => \Illuminate\Auth\Middleware\Authorize::class,
         'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
         'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
         'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
         'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
         'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
     ];
 
     /* ... */
 }

Customize Worker Behavior

[!TIP] You can register the JobInitializerInterface implementation to change workers' behavior.

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Mpyw\LaravelCachedDatabaseStickiness\JobInitializers\AlwaysFreshInitializer;
use Mpyw\LaravelCachedDatabaseStickiness\JobInitializers\JobInitializerInterface;

class DatabaseServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(JobInitializerInterface::class, AlwaysFreshInitializer::class);
    }
}
General QueueableShouldAssumeFresh QueueableShouldAssumeModified Queueable
AlwaysModifiedInitializer<br>(Default)MasterSlaveMaster
AlwaysFreshInitializerSlaveSlaveMaster

Attention

[!CAUTION]

Don't call Schema::defaultStringLength() in ServiceProvider::boot()

Problem

Assume that you have the following ServiceProvider.

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Schema::defaultStringLength(191);
    }
}

If you run composer install or directly call php artisan pacakge:discover, it will unexpectedly use caches. It will trigger errors when we execute the command in the environment unreachable to the cache repository.

RedisException  : Operation timed out

Solution

Directly use Illuminate\Database\Schema\Builder. Don't call via Illuminate\Support\Facades\Schema Facade.

 <?php
 
 namespace App\Providers;

-use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Builder as SchemaBuilder;
 use Illuminate\Support\ServiceProvider;
 
 class AppServiceProvider extends ServiceProvider
 {
     /**
      * Bootstrap any application services.
      *
      * @return void
      */
     public function boot()
     {
-        Schema::defaultStringLength(191);
+        SchemaBuilder::defaultStringLength(191);
     }
 }