Performance

Cache pools

Cache the slow answers and compute them once. The pool stores the result under a key, and the closure rebuilds it only when the key is missing or expired. Reports, menus, and counters are typical first candidates.

use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; class ReportBuilder { public function __construct(private CacheInterface $cache) { } public function monthly(string $month): array { return $this->cache->get("report_" . $month, function (ItemInterface $item): array { $item->expiresAfter(3600); return ["month" => "todo", "total" => 0]; }); } }

OPcache on

In production PHP must not recompile every file on each request. OPcache keeps the compiled code in memory, which is the largest free speedup available, and it needs only two settings to start.

opcache.enable=1, opcache.validate_timestamps=0

PHP serves compiled code from memory, no recompile per request

Query counts

Most slow pages are slow database loops: one query for the list plus one query per row. Fetch the relation in the same query with a join, then compare the query count before and after in the debug toolbar.

$posts = $repo->createQueryBuilder("p") ->leftJoin("p.comments", "c") ->addSelect("c") ->getQuery() ->getResult();

Measure first

Optimize only what you measured. The dev toolbar shows page time and query count on every page, so write both numbers down, change one thing, and compare. One fix at a time beats a full rewrite.

Before: 240 ms, 35 queries

After join fetch: 60 ms, 2 queries

Next: Capstone: blog

Article author: Arthur Isaev