fsi/resource-repository-bundle
A Symfony bundle that integrates FSi's resource repository library, offering reusable repository services for Doctrine entities with built-in CRUD, pagination, and filtering to reduce boilerplate and keep persistence logic out of controllers.
This is a small, single-purpose Symfony bundle: it gives you a Doctrine-backed key/value store where the "value" is a PHP resource object. There is no Flex recipe and very little documentation, so the first hour is best spent in the source, not the README.
composer require fsi/resource-repository-bundle
// config/bundles.php
FsOpen\Bundle\ResourceRepositoryBundle\FsOpenResourceRepositoryBundle::class => ['all' => true],
bin/console doctrine:mapping:info
bin/console doctrine:schema:update --dump-sql # or generate a migration
If the bundle's resource entity is not listed, your doctrine.orm.auto_mapping is probably disabled — register the bundle's mapping explicitly under doctrine.orm.mappings before going further.
Read, in this order: Model/AbstractResource.php, Repository/ResourceRepository.php, the Doctrine entity, and Tests/. Those four files are effectively the whole public API.
First use case — a global settings blob for an admin panel. Subclass the abstract resource and give it a stable name:
namespace App\Resource;
use FsOpen\Bundle\ResourceRepositoryBundle\Model\AbstractResource;
class SettingsResource extends AbstractResource
{
private string $siteName = 'My App';
private bool $maintenanceMode = false;
public function getResourceName(): string { return 'app_settings'; }
// getters/setters...
}
$repo = $this->container->get('fsi_resource_repository.repository');
$settings = $repo->get('app_settings') ?? new SettingsResource();
$settings->setSiteName('Acme');
$repo->persist($settings);
That round-trip — instantiate, populate, persist, fetch by name — is the entire happy path. Verify method names against the installed version before copying snippets; the API is small and has changed little, but there is no upgrade guide to lean on.
Wrap the repository, never inject it directly. The service id is stringly-typed and the return type is loosely typed (null for missing keys). Put a typed service in front of it:
final class SettingsProvider
{
public function __construct(
private object $repository, // fsi_resource_repository.repository
private CacheInterface $cache,
) {}
public function get(): SettingsResource
{
return $this->cache->get('app_settings', function () {
return $this->repository->get('app_settings') ?? new SettingsResource();
});
}
public function save(SettingsResource $settings): void
{
$this->repository->persist($settings);
$this->cache->delete('app_settings');
}
}
Use a DI alias or services.yaml binding so application code only sees SettingsProvider.
Pick one granularity and stick to it. Two viable shapes:
app_settings, invoice_defaults, mailer_config. Simple, cacheable, easy to reason about.invoice_42_metadata containing an array or a small DTO. Handy for arbitrary per-record attributes, but you lose any ability to query them.Do not mix the two; the naming scheme is your only index.
Treat persist as a read-modify-write transaction. The pattern is fetch → mutate → persist, which is inherently racy. Wrap it in $em->transactional() or serialize writes through a single command/consumer if the resource is hot (settings toggled by admins during traffic is the classic lost-update case).
Admin forms map cleanly. Bind a Symfony form to your resource class, persist in the controller after isValid(). Because the resource is a plain object, data_class works without any custom mapper — this is where the bundle earns its keep versus hand-rolled settings tables.
Fixtures and seeds. Create resources idempotently (get() ?? new ...) so doctrine:fixtures:load can run repeatedly. Never INSERT rows by hand unless you know the serialization format the repository expects.
Testing. Boot a kernel with SQLite in-memory, create the schema, and assert a round-trip for every resource class you add. This is the cheapest insurance against serialization drift.
Migration path. If you already have an app_settings table, import each row into a resource in one data migration, then drop the table only after a release where reads go exclusively through the repository.
private property renames, and consider a version field for future migrations.get() returns null for unknown keys (and may throw for malformed payloads). Always null-coalesce, and add a try/catch around fetches of resources written by older code.get() is a DB hit. Decorate the repository service (it's a normal service, so decorates: works) or wrap it as shown above.TEXT column caps out around 64KB on MySQL; long JSON payloads will truncate or error. Verify with doctrine:schema:update --dump-sql before storing anything sizable.doctrine:mapping:info first whenever something feels wrong.SELECT * FROM ...) and unserialize/json_decode the payload in bin/console doctrine:query:sql or a tinker session.composer.json, read the diff before upgrading, and assume you may need to fork — MIT licensing makes vendoring or a maintained fork a realistic option.AbstractResource, decorate the repository service, and override the entity class if the bundle's configuration exposes it. There is no event system, so cross-cutting concerns (audit logging, cache invalidation) go in your decorator.How can I help you explore Laravel packages today?