phil / edit-lock-bundle
Edit leases for Symfony applications: one writer per resource or per block of a UJO (Universal JSON Output) document, with heartbeats, expiry, takeover and release, kept by Doctrine, and presence and block saves pushed to the other writers through Mercure as Turbo Streams, applied in the browser by
Package info
github.com/parler-haut-interagir-librement/edit-lock-bundle
Type:symfony-bundle
pkg:composer/phil/edit-lock-bundle
Requires
- php: >=8.5
- ext-filter: *
- ext-mbstring: *
- doctrine/dbal: ^4.5
- doctrine/orm: ^3.7
- phil/ujo: ^0.1@dev
- psr/clock: ^1.0
- psr/log: ^3.0
- symfony/config: ^8.2
- symfony/dependency-injection: ^8.2
- symfony/http-foundation: ^8.2
- symfony/security-csrf: ^8.2
Requires (Dev)
- ext-pdo: *
- doctrine/doctrine-bundle: ^3.3
- doctrine/persistence: ^4.1
- phil/ujo-bundle: ^0.1@dev
- phpunit/phpunit: ^13.4
- symfony/asset-mapper: ^8.2
- symfony/browser-kit: ^8.2
- symfony/clock: ^8.2
- symfony/event-dispatcher: ^8.2
- symfony/framework-bundle: ^8.2
- symfony/http-client: ^8.2
- symfony/http-kernel: ^8.2
- symfony/mercure: ^0.10
- symfony/mercure-bundle: ^0.7
- symfony/routing: ^8.2
Suggests
- phil/block-editor-bundle: For the block editor the controller works with
- phil/ujo-bundle: To check saved blocks with the application's block types
- symfony/asset-mapper: To serve the controller and its stylesheet without a build step
- symfony/mercure-bundle: To push presence and block saves to the other writers
- symfony/routing: For the lease and block endpoints, and their URLs given to the controller
- symfony/stimulus-bundle: For the collaboration controller
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-11 13:30:15 UTC
README
One writer per block: edit leases, presence and block saves pushed through Mercure, for Symfony applications.
phil/edit-lock-bundle lets several people edit the same resource without overwriting each other. A writer who enters
a block of a document leases it: nobody else can write that block until the lease is released, expires for want of a
heartbeat, or is taken over by someone the application allows, an owner or an administrator. A lease can also cover a
whole resource, which then excludes every lease of others on its parts. Each block the holder saves is checked by
phil/ujo, handed to the application, and pushed to the other writers with the list of who edits what, as Turbo
Streams through a Mercure hub. In the browser, a Stimulus controller takes the leases as the writer moves from block
to block, closes and names the blocks others hold, and applies their saves to the editor of
phil/block-editor-bundle as they arrive. Granularity is the block: there is no merge of characters.
It is for any Symfony application where people write together: a wiki page, a shared draft, a game master's notes,
the minutes of a meeting. The application names the resources (article:42, wiki/home), says who writes and what
they may edit, and decides where saved blocks go; the bundle keeps the leases and carries the messages. A resource
need not be a document: a form, a record or a file can be leased whole, from PHP, without the controller.
It is a Symfony bundle on phil/ujo, Doctrine and, optionally, MercureBundle. Leases live in one table, written by
compare-and-set queries in a transaction that locks the resource's row, so that two requests racing for a block, or
for the whole resource and one of its blocks, never both win; or in memory for tests. Every endpoint checks the
controller's header and Symfony's stateless CSRF token. Without
Mercure the controller asks for presence at each heartbeat and saves are not pushed. The controller is served by
AssetMapper, with no build step. Mercure, Turbo Streams and UJO (Universal JSON Output) are the formats it speaks; it
depends on no other lock or collaboration library.
Status
| Part | State |
|---|---|
LeaseKey, Holder, Lease: a resource or a block, its holder, until when |
Implemented and tested |
EditLocks: acquire, heartbeat, release, leave, take over, presence, purge |
Implemented and tested |
LeaseRepository; DoctrineLeaseRepository by compare-and-set, one claim at a time per resource; InMemoryLeaseRepository |
Implemented and tested, on SQLite, and two concurrent claims on MySQL and PostgreSQL |
BlockSaves: blocks checked by phil/ujo, written by their holder, kept by the application's BlockStore |
Implemented and tested |
MercureBroadcaster, TurboStreams: presence, saved and removed blocks as Turbo Streams |
Implemented and tested, also against a real hub |
EditLockController, routes, ClientConfig: the endpoints and the controller's configuration |
Implemented and tested |
The collaboration controller: leases that follow the writer, roster, takeover, remote saves applied to the block editor |
Implemented; pure modules tested under Node, checked in a browser with two writers (testing) |
| Merging the characters two writers type in one block | Not part of the bundle: one writer per block |
Documentation
| Document | What it covers |
|---|---|
| Installation | Requirements, enabling the bundle, the table, the routes, Mercure, the controller and the stylesheet. |
| Configuration | Every option, and the services the application writes. |
| Leases | Keys, holders, durations, heartbeats, expiry, takeover, release, presence and the races between requests. |
| Blocks | Saving and removing blocks, the check by phil/ujo, the application's block store. |
| Mercure | Topics, private updates, the Turbo Streams pushed, and running without a hub. |
| Endpoints | Each route, its body, its answers, the controller's header and its CSRF token. |
| JavaScript | The controller, its configuration, its modules, and how it works with the block editor. |
| Exceptions | The bundle's exceptions, and the answers of its endpoints. |
| Extension points | What an application implements, configures, replaces or relies on. |
| Testing | The PHP and Node tests, the real hub, the mutation bar and the checks in a browser. |
| Reference | Every class and module of the bundle, with its role. |
Quick start
composer require phil/edit-lock-bundle symfony/mercure-bundle
# config/packages/phil_edit_lock.yaml phil_edit_lock: holder_provider: App\Collaboration\CurrentWriter # a HolderProvider policy: App\Collaboration\WritersPolicy # an EditPolicy block_store: App\Collaboration\DraftBlocks # a BlockStore, optional
<div data-controller="phil--edit-lock-bundle--collaboration" data-phil--edit-lock-bundle--collaboration-config-value="{{ collaboration|json_encode }}"> {{ form_row(form.body) }} {# the block editor of phil/block-editor-bundle #} <aside data-phil--edit-lock-bundle--collaboration-target="presence"></aside> </div>
collaboration is ClientConfig::for('article:42', $holder), given by the page's controller.
Requirements
PHP 8.5 or later, Symfony 8.2, Doctrine DBAL 4 and ORM 3, phil/ujo. For pushed updates, MercureBundle and a Mercure
hub; for the controller, Stimulus 3 and phil/block-editor-bundle.
Contributing
Contributions are welcome on the PHIL monorepo: read the contribution guide and the code of conduct. Report a vulnerability privately, as the security policy explains, never in a public issue.
License
MIT, © Philippe Gamache, doing business as Le Gnome Archiviste; a Parler Haut Interagir Librement project. See LICENSE.