Skip to content

phil/edit-lock-bundle

alpha

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

PartState
LeaseKey, Holder, Lease: a resource or a block, its holder, until whenImplemented and tested
EditLocks: acquire, heartbeat, release, leave, take over, presence, purgeImplemented and tested
LeaseRepository; DoctrineLeaseRepository by compare-and-set, one claim at a time per resource; InMemoryLeaseRepositoryImplemented 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 BlockStoreImplemented and tested
MercureBroadcaster, TurboStreams: presence, saved and removed blocks as Turbo StreamsImplemented and tested, also against a real hub
EditLockController, routes, ClientConfig: the endpoints and the controller's configurationImplemented and tested
The collaboration controller: leases that follow the writer, roster, takeover, remote saves applied to the block editorImplemented; pure modules tested under Node, checked in a browser with two writers (testing)
Merging the characters two writers type in one blockNot part of the bundle: one writer per block

Documentation

DocumentWhat it covers
InstallationRequirements, enabling the bundle, the table, the routes, Mercure, the controller and the stylesheet.
ConfigurationEvery option, and the services the application writes.
LeasesKeys, holders, durations, heartbeats, expiry, takeover, release, presence and the races between requests.
BlocksSaving and removing blocks, the check by phil/ujo, the application's block store.
MercureTopics, private updates, the Turbo Streams pushed, and running without a hub.
EndpointsEach route, its body, its answers, the controller's header and its CSRF token.
JavaScriptThe controller, its configuration, its modules, and how it works with the block editor.
ExceptionsThe bundle's exceptions, and the answers of its endpoints.
Extension pointsWhat an application implements, configures, replaces or relies on.
TestingThe PHP and Node tests, the real hub, the mutation bar and the checks in a browser.
ReferenceEvery 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.