Skip to content

Installation

phil/edit-lock-bundle

Requirements

PHP 8.5 or later, Symfony 8.2 with FrameworkBundle and its CSRF protection (symfony/security-csrf), DoctrineBundle with DBAL 4 and ORM 3, a clock service (symfony/clock), and phil/ujo. phil/ujo-bundle is recommended: saved blocks are then checked with the application's own block types. To push presence and saves, MercureBundle and a Mercure hub. In the browser, Stimulus 3 served by symfony/stimulus-bundle and AssetMapper, and the block editor of phil/block-editor-bundle.

Enabling the bundle

composer require phil/edit-lock-bundle
// config/bundles.php
return [
    // …
    Phil\EditLockBundle\PhilEditLockBundle::class => ['all' => true],
];

The tables

The bundle maps Phil\EditLockBundle\Domain\Model\Lease and LeasedResource for Doctrine, as the tables phil_edit_lease and phil_edit_lease_resource: create them with the application's migrations.

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate
ColumnTypeRole
resourceVARCHAR(255), primary keyThe resource, as the application names it
partVARCHAR(64), primary keyThe block's identifier; empty for the whole resource
holder_id, holder_nameVARCHAR(255)Who holds it, and the name the others see
acquired_at, expires_atDATETIME, UTCSince and until when; expires_at is indexed
versionINTEGEROne more at each change, for compare-and-set

phil_edit_lease_resource holds one row per resource ever leased, the row each claim locks (races):

ColumnTypeRole
resourceVARCHAR(255), primary keyThe resource, as the application names it
claimsINTEGERHow many claims ran on it

The application's services

The endpoints answer only once the application has said who writes and what they may edit:

# config/packages/phil_edit_lock.yaml
phil_edit_lock:
    holder_provider: App\Collaboration\CurrentWriter
    policy: App\Collaboration\WritersPolicy
use Phil\EditLockBundle\Application\HolderProvider;
use Phil\EditLockBundle\Domain\ValueObject\Holder;
use Symfony\Bundle\SecurityBundle\Security;

final readonly class CurrentWriter implements HolderProvider
{
    public function __construct(private Security $security) {}

    public function holder(): ?Holder
    {
        $user = $this->security->getUser();

        return null === $user ? null : Holder::of($user->getUserIdentifier(), $user->getUserIdentifier());
    }
}

The policy is described in leases, the block store in blocks.

The routes

# config/routes/phil_edit_lock.yaml
phil_edit_lock:
    resource: '@PhilEditLockBundle/config/routes.php'
    prefix: /_edit-lock

Put the prefix behind the application's firewall. The routes are listed in endpoints. The bundle adds the token phil_edit_lock to FrameworkBundle's csrf_protection.stateless_token_ids, which turns framework.csrf_protection on: leave it on. With csrf_protection: false, the container stops with a LogicException that says so.

Mercure

With MercureBundle registered, the bundle publishes to its default hub. Updates are private: each writer's browser needs the right to subscribe to the topic of the resource, which the application grants as MercureBundle explains, for instance with its authorization cookie:

$authorization->setCookie($request, [$config['topic']]); // the topics this writer may subscribe to

Mercure has the topics, the messages and the options.

The controller and the stylesheet

With AssetMapper, the bundle maps its controller under @phil/edit-lock-bundle and its stylesheet under phil-edit-lock. Enable the controller in assets/controllers.json:

{
    "controllers": {
        "@phil/edit-lock-bundle": {
            "collaboration": { "enabled": true, "fetch": "eager" }
        }
    }
}
<link rel="stylesheet" href="{{ asset('phil-edit-lock/edit-lock.css') }}">

JavaScript shows the markup and the configuration.