shapecode/repository-as-a-service
Shapecode's Repository as a Service brings repository-pattern abstractions to Laravel, helping decouple data access from business logic, keep models and controllers lean, and swap or reuse query implementations behind a clean, testable service layer.
This is a Symfony2-era bundle (not Laravel), so the first step is confirming your app is on a compatible Symfony/Doctrine ORM version before anything else — the bundle predates Symfony Flex autoconfiguration and expects the classic bundle registration flow.
composer require shapecode/repository-as-a-service-bundle
app/AppKernel.php (Symfony 2/3) under $bundles, or in config/bundles.php if you're on a Symfony 4-style app that still tolerates the older bundle API.DependencyInjection/Configuration.php first. That file is the source of truth for the config alias and available options — the README is likely thin, and guessing the alias (shapecode_repository_as_a_service vs. a shorter shorthand) wastes more time than opening the file.Repository/ or Doctrine/ in the bundle. That class is what your own repositories will extend or implement, and it's where the EntityManager/Registry is wired in.CompilerPass, you'll need a tag or a naming convention on your classes. If it registers via YAML service definitions, skip the tag question entirely.First use case: take one bloated controller action — the one with a 30-line DQL query inline — and move that query into a repository class registered through the bundle. Inject the repository into the controller constructor and confirm the container resolves it. That single round-trip validates install, registration, config, and DI all at once.
Service-per-repository. The whole point of the bundle is that repositories become container-managed services rather than $em->getRepository(Entity::class) lookups. Write your repository with constructor injection of the entity manager (or whatever the bundle's base class expects) and let the container own the lifecycle:
class OrderRepository extends BaseRepository
{
public function findRecentForCustomer(Customer $customer, int $limit = 20): array
{
return $this->createQueryBuilder('o')
->where('o.customer = :customer')
->setParameter('customer', $customer)
->orderBy('o.createdAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
}
Thin controller, fat repository. Keep controllers to input validation, a repository call, and a response. All DQL/QueryBuilder work lives behind the repository interface. This is the pattern the bundle is designed to enable and the one that pays off day to day.
Interface binding for testability. Define an OrderRepositoryInterface, have the concrete class implement it, and alias the interface to the concrete service in services.yml. This is the single highest-value addition — it lets you swap in an in-memory fake in unit tests instead of booting Doctrine.
Autowiring by type-hint. In Symfony 3.3+ apps, type-hint the repository interface in a constructor and let autowiring resolve it. If the bundle doesn't autoconfigure, add explicit autowire: true and public: false to your services.yml block for each repository.
Tag-based bulk registration. If the bundle ships a compiler pass, tag every repository once in services.yml:
App\Repository\:
resource: '../src/Repository/*'
tags: ['shapecode.repository']
Verify the exact tag name in the compiler pass source — a typo here fails silently, and you'll only notice when autowiring throws a ServiceNotFoundException at runtime.
Composition over inheritance. Where possible, wrap the bundle's base repository with a small trait or delegate rather than extending it deeply. If the bundle is later abandoned (likely, at 2 stars), swapping the dependency is a one-file change instead of an audit of every repository in the codebase.
Read/write separation. Use the service registry from the bundle for read queries, but keep transactional write logic in command handlers or application services. Repositories shouldn't call flush() on their own — transaction boundaries belong one layer up.
composer.json, commit composer.lock, and budget for a fork or inline replacement if you upgrade Symfony.each()-era idioms, untyped properties, or Symfony\Component\DependencyInjection\Definition APIs that changed. Run composer why-not and a smoke test before assuming it works.cache:clear (and in dev, sometimes cache:warmup). This is the #1 confusing failure mode: "the class exists, the tag exists, the container disagrees."X will ignore classes tagged Y with no warning. Always confirm the tag string against CompilerPass/*.php, not the README.$em->getRepository(Entity::class) with the bundle's service repositories for the same entity. You'll get two instances with subtly different state expectations and unpredictable behavior around clear() and identity maps.doctrine.orm.entity_manager reference breaks multi-EM setups.bin/console debug:container App\Repository\OrderRepository and debug:container --tag=<tag> are the fastest way to see whether the bundle actually registered your class. Use them before touching YAML.src/, decorate services with Symfony's decorates: key rather than editing vendor code, and consider wrapping the bundle's registration in a small custom compiler pass of your own so you control the tag name and can drop the bundle later without touching application code.services.yml plus an abstract base class — you don't need the bundle to get the architecture, only the registration sugar. Weigh that against the maintenance cost before committing.How can I help you explore Laravel packages today?