loicpennamen/sf-shared-repository
A shared repository package for Symfony applications that provides a reusable base repository layer, centralizing common data-access logic and reducing duplication across entities while keeping persistence code consistent and maintainable. Ideal for teams seeking a clean, reusable data-access founda...
This is not a Laravel package. It targets Symfony 4 + Doctrine ORM, replacing ServiceEntityRepository (not Eloquent) with a repository that understands the query-string contract DataTables server-side processing sends (draw, start, length, search[value], order[0][column], columns[i][data]). There is no README worth speaking of, no tags, no tests you can rely on, and zero dependents — treat it as vendored source you are adopting, not a dependency you are installing. Before you write a line of integration code, clone it and read src/; the class is a few hundred lines and every behaviour below has to be confirmed against what is actually there.
1. Pull it in by commit, not by version.
"repositories": [
{ "type": "vcs", "url": "https://bitbucket.org/LoicPennamen/sf-shared-repository.git" }
],
"require": { "loicpennamen/sf-shared-repository": "dev-master#<sha>" }
Pin the SHA. Bitbucket repos with no tags move under you.
2. Wire it in the Doctrine way. The class is meant to be used as the repositoryClass on an entity mapping, so getRepository() returns it instead of the default:
/**
* @ORM\Entity(repositoryClass=ProductRepository::class)
*/
class Product { /* ... */ }
class ProductRepository extends SharedRepository { }
If instead it is a service (constructor taking ManagerRegistry, as Symfony 4.2+ ServiceEntityRepository does), you register it under services.yaml with App\Repository\ resource loading. Check which one it is — this determines whether the entity mapping or the container is your integration point, and it is the single most common source of "repository has no EntityManager" errors.
3. First use case: a server-side DataTables endpoint. One controller action returning JSON, nothing else:
public function list(Request $request, ProductRepository $repo): JsonResponse
{
return $this->json($repo->searchForDataTables($request->query->all()));
}
The payload shape you must return is DataTables', not Symfony's: draw echoed back, recordsTotal, recordsFiltered, and data. Confirm whether the package builds that envelope or only returns results — that difference decides how much glue you own.
Where to look first: src/ for the public method that accepts the request array, then whatever column/join configuration it expects (protected properties to override, or an argument to the call). Everything else in this package hangs off that one signature.
Keep the controller thin. The value of this class is that filtering, sorting, and pagination for a table live in one repository method instead of a 200-line controller. Resist the urge to post-process rows in the controller; if you need computed columns, do it in the repository or accept the extra pass. Laravel developers coming from yajra/laravel-datatables will look for addColumn/editColumn equivalents — there are none, so plan on array hydration plus a small mapping step if you need derived fields.
Whitelist columns in the repository, not the request. DataTables sends column identity as an index into columns[i][data], and that string arrives from the browser. Any repository worth using here maps it against a declared list before touching DQL:
protected $searchable = ['p.name', 'p.sku', 'c.name'];
protected $sortable = ['p.name' => 'p.name', 'category' => 'c.name'];
If the package does not already do this (verify), wrap the call and normalise the request array first. A column name interpolated into orderBy() is an injection point that Doctrine will happily execute.
Joins are where the "powerful" part lives and where it breaks. Searching across associations means a leftJoin on the query builder. Two consequences:
recordsFiltered must be COUNT(DISTINCT p.id). Get this wrong and your pager silently lies.Pagination: setFirstResult($start)->setMaxResults($length) on the data query, a separate SELECT COUNT(...) for the totals. Use array hydration (getArrayResult() / getQuery()->getArrayResult()) for tables — you are serialising to JSON anyway and it halves the cost.
Multi-term search. Users type "acme blue" and expect products matching both terms somewhere. Split on whitespace and AND the terms, OR-ing each across your searchable columns:
foreach (preg_split('/\s+/', trim($term)) as $word) {
$or = $qb->expr()->orX();
foreach ($this->searchable as $col) {
$or->add($qb->expr()->like($col, ':w'));
}
$qb->andWhere($or)->setParameter('w', '%'.$word.'%');
}
Per-entity subclasses. The intended pattern is one small subclass per entity that declares its searchable columns, sortable map, and default order; the base class does the request parsing and counting. Keep the customisation surface data-only where possible so the base stays upgradeable — which matters more than usual given there is no maintainer.
In a Laravel codebase, you have two honest options: run this only if you are already on Doctrine via laravel-doctrine/orm (the repository works unchanged; you just wire the registry into the container), or skip it and use yajra/laravel-datatables with Eloquent. Porting the class to Eloquent is a rewrite of the query-builder half, and you would be maintaining a fork of an unmaintained package — the cost is almost never worth it for a single table.
ServiceEntityRepository and EntityRepository changed shape between Doctrine 2 and 3 (ManagerRegistry constructor, EntityRepository deprecation). On Symfony 5.4+/Doctrine 3 expect to patch the constructor yourself. Symfony 4 is EOL, so this is not a hypothetical.columns[i][search][value] on every request, usually empty. Do not treat the presence of per-column search parameters as an active filter — check for non-empty strings, or every query gets a LIKE '%%' on every column and the planner gives up.draw back unchanged. Mismatched or missing draw makes DataTables discard the response and retry, and you get an infinite request loop that looks like a server bug.recordsTotal vs recordsFiltered. Total is the unfiltered count; filtered is what matches the search. Swapping them produces a table that renders but whose "Showing X of Y" is nonsense.order array. DataTables omits it when ordering is disabled; always have a deterministic fallback ORDER BY id or pagination returns overlapping pages.$qb->getQuery()->getSQL() plus getParameters() inside the profiler, or the Symfony Doctrine panel. Most failures here are a missing join on the count query, which no exception will tell you about — the number is just wrong.App\Repository namespace with a note of its origin, patch it freely, and stop pretending it is a third-party dependency.How can I help you explore Laravel packages today?