Skip to content

Commit

Permalink
Feat: Add Stickiness Resolver Middleware (#7)
Browse files Browse the repository at this point in the history
* Feat: Add Stickiness Resolver Middleware

* Doc: Update README

* Test: Add feature tests
  • Loading branch information
mpyw authored Jan 2, 2020
1 parent 26e241d commit 59cdf0e
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 4 deletions.
80 changes: 76 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,82 @@ class DatabaseServiceProvider extends ServiceProvider
}
```

| | Source |
|:---:|:---:|
| `IpBasedResolver`<br>**(Default)**| Remote IP address |
| `AuthBasedResolver` | Authenticated User ID |
| | Source | Middleware |
|:---:|:---:|:---:|
| `IpBasedResolver`<br>**(Default)**| Remote IP address | |
| `AuthBasedResolver` | Authenticated User ID | Required |

You must add **`ResolveStickinessOnResolvedConnections`** middleware before `Authenticate`
when you use `AuthBasedResolver`.

```diff
--- 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

Expand Down
47 changes: 47 additions & 0 deletions src/Http/Middleware/ResolveStickinessOnResolvedConnections.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Mpyw\LaravelCachedDatabaseStickiness\Http\Middleware;

use Closure;
use Illuminate\Database\DatabaseManager;
use Illuminate\Http\Request;
use Mpyw\LaravelCachedDatabaseStickiness\StickinessManager;

class ResolveStickinessOnResolvedConnections
{
/**
* @var \Mpyw\LaravelCachedDatabaseStickiness\StickinessManager
*/
protected $stickiness;

/**
* @var \Illuminate\Database\DatabaseManager
*/
protected $db;

/**
* ResolveStickinessOnResolvedConnections constructor.
*
* @param \Mpyw\LaravelCachedDatabaseStickiness\StickinessManager $stickiness
* @param \Illuminate\Database\DatabaseManager $db
*/
public function __construct(StickinessManager $stickiness, DatabaseManager $db)
{
$this->stickiness = $stickiness;
$this->db = $db;
}

/**
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
foreach ($this->db->getConnections() as $connection) {
$this->stickiness->resolveRecordsModified($connection);
}

return $next($request);
}
}
126 changes: 126 additions & 0 deletions tests/Feature/Http/Middleware/MiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace Mpyw\LaravelCachedDatabaseStickiness\Tests\Feature\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Http\Kernel as KernelContract;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Route;
use Mpyw\LaravelCachedDatabaseStickiness\ConnectionServiceProvider;
use Mpyw\LaravelCachedDatabaseStickiness\Http\Middleware\ResolveStickinessOnResolvedConnections;
use Mpyw\LaravelCachedDatabaseStickiness\StickinessResolvers\AuthBasedResolver;
use Mpyw\LaravelCachedDatabaseStickiness\StickinessResolvers\StickinessResolverInterface;
use Mpyw\LaravelCachedDatabaseStickiness\StickinessServiceProvider;
use Mpyw\LaravelCachedDatabaseStickiness\Tests\Stubs\Models\User;
use Orchestra\Testbench\Http\Kernel;
use Orchestra\Testbench\TestCase;
use PDO;
use ReflectionProperty;

abstract class MiddlewareTest extends TestCase
{
/**
* @var null|bool
*/
protected $withMiddleware;

/**
* @var string
*/
protected $database;

/**
* @param \Illuminate\Foundation\Application $app
* @return array
*/
protected function getPackageProviders($app): array
{
return [
StickinessServiceProvider::class,
ConnectionServiceProvider::class,
];
}

/**
* @param \Illuminate\Foundation\Application $app
*/
protected function getEnvironmentSetUp($app): void
{
$app['config']->set('database.default', 'test');
$app['config']->set('database.connections.test', [
'driver' => 'sqlite',
'database' => $this->database = tempnam(storage_path(''), 'sqlite_'),
'sticky' => true,
]);
$app['config']->set('auth.providers.users.model', User::class);
$app->bind(StickinessResolverInterface::class, AuthBasedResolver::class);
}

protected function setUp(): void
{
parent::setUp();

$pdo = new PDO("sqlite:$this->database", null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo->exec('drop table if exists users');
$pdo->exec('create table users(
id integer primary key autoincrement,
email text not null,
password text not null,
created_at datetime,
updated_at datetime
)');
$stmt = $pdo->prepare('insert into users(email, password) values (?, ?)');
$stmt->execute(['[email protected]', Hash::make('password')]);

Route::middleware('auth.basic')->get('/', function () {
return ['message' => 'ok'];
});
}

protected function tearDown(): void
{
parent::tearDown();

@unlink($this->database);
}

protected function resolveApplicationHttpKernel($app)
{
$app->singleton(KernelContract::class, function ($app) {
return new class($app, $app['router'], $this->withMiddleware) extends Kernel {
public function __construct(Application $app, Router $router, bool $withMiddleware)
{
$this->middlewareGroups['auth'] = array_filter([
Authenticate::class,
$withMiddleware ? ResolveStickinessOnResolvedConnections::class : null,
]);
$this->middlewareGroups['auth.basic'] = array_filter([
AuthenticateWithBasicAuth::class,
$withMiddleware ? ResolveStickinessOnResolvedConnections::class : null,
]);

unset($this->routeMiddleware['auth'], $this->routeMiddleware['auth.basic']);

parent::__construct($app, $router);
}
};
});
}

protected function getRecordsModifiedViaReflection()
{
/* @var \Illuminate\Database\Connection $connection */
$connection = DB::connection();

/* @noinspection PhpUnhandledExceptionInspection */
$property = new ReflectionProperty($connection, 'recordsModified');
$property->setAccessible(true);
return $property->getValue($connection);
}
}
40 changes: 40 additions & 0 deletions tests/Feature/Http/Middleware/WithMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Mpyw\LaravelCachedDatabaseStickiness\Tests\Feature\Http\Middleware;

use Illuminate\Contracts\Cache\Repository as CacheRepository;

class WithMiddlewareTest extends MiddlewareTest
{
protected $withMiddleware = true;

public function testWithMiddlewareWhenCacheExists(): void
{
$this->mock(CacheRepository::class)
->shouldReceive('has')
->with('database-stickiness:connection=test,resolver=auth,id=1')
->once()
->andReturnTrue();

$this->get('/', [
'Authorization' => 'Basic ' . base64_encode('[email protected]:password'),
])->assertSuccessful();

$this->assertTrue($this->getRecordsModifiedViaReflection());
}

public function testWithMiddlewareWhenCacheDoesNotExist(): void
{
$this->mock(CacheRepository::class)
->shouldReceive('has')
->with('database-stickiness:connection=test,resolver=auth,id=1')
->once()
->andReturnFalse();

$this->get('/', [
'Authorization' => 'Basic ' . base64_encode('[email protected]:password'),
])->assertSuccessful();

$this->assertFalse($this->getRecordsModifiedViaReflection());
}
}
22 changes: 22 additions & 0 deletions tests/Feature/Http/Middleware/WithoutMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Mpyw\LaravelCachedDatabaseStickiness\Tests\Feature\Http\Middleware;

use Illuminate\Contracts\Cache\Repository as CacheRepository;

class WithoutMiddlewareTest extends MiddlewareTest
{
protected $withMiddleware = false;

public function testWithoutMiddleware(): void
{
$this->mock(CacheRepository::class)
->shouldNotReceive('has');

$this->get('/', [
'Authorization' => 'Basic ' . base64_encode('[email protected]:password'),
])->assertSuccessful();

$this->assertFalse($this->getRecordsModifiedViaReflection());
}
}
12 changes: 12 additions & 0 deletions tests/Stubs/Models/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Mpyw\LaravelCachedDatabaseStickiness\Tests\Stubs\Models;

use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Database\Eloquent\Model;

class User extends Model implements UserContract
{
use Authenticatable;
}

0 comments on commit 59cdf0e

Please sign in to comment.