Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Quality Tools Laravel Package

nowo-tech/php-quality-tools

A Laravel/PHP package that bundles and configures quality tooling such as PHPStan, Pint, PHPCS and Rector, with ready-made configs and Composer scripts so you can enforce coding standards, run static analysis and automate refactoring with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

What you are actually installing. A dev-only Composer package with two distinct halves:

  1. Shared configs for Rector, PHP-CS-Fixer and Twig-CS-Fixer (unchanged) — files you require from your project root.
  2. Quality gate binaries (new in v1.6.0) exposed via bin: check-no-inline-svg, check-asset-budgets, check-doc-links. These are standalone console commands that scan your repo and exit non-zero on failure. They are not artisan commands, register no service provider, and add nothing to require.

Both halves belong in require-dev and must never leak into a deploy artifact's autoload path.

composer require --dev nowo-tech/php-quality-tools
ls vendor/bin   # now six-ish binaries, not three — never guess names

Where to look first, in this order:

  1. The package composer.json — it tells you which upstream tools and versions it pins, and (important for v1.6.0) that symfony/yaml is only a suggestion, not a requirement. That single line determines whether the YAML asset-budget config is usable for you.
  2. The package's config/ (or resources/config/, etc/) directory inside vendor/nowo-tech/php-quality-tools/ — the files you will require. Confirm whether each returns an array or a constructed object (PhpCsFixer\Config, RectorConfig closure, Twig config array); that dictates your merge strategy.
  3. USAGE.md — specifically the new Front-end and docs gates (since 1.6.0) section. This is now the authoritative reference for flags and defaults, more so than the README.
  4. bin/ — read the three new commands' --help output (vendor/bin/check-asset-budgets --help). Defaults are Symfony-flavoured (templates/, assets/, .twig), which is exactly the kind of thing you must verify rather than assume.
  5. CHANGELOG and latest tag — one maintainer, forward-dated releases: verify the tag you install is the tag you reviewed.

First use case — one tool, one path, dry-run only. Don't adopt six tools at once. Start with Rector on a single directory:

vendor/bin/rector process app/Models --dry-run

Then, because the new gates are cheap and read-only, add them in ascending order of noise:

vendor/bin/check-doc-links                       # cheapest, near-zero false positives
npm run build && vendor/bin/check-asset-budgets  # needs a real Vite manifest
vendor/bin/check-no-inline-svg --help            # configure paths before first real run

Only once the Rector diff is small and reviewable do you wire the shared PHP-CS-Fixer config, and only then Twig-CS-Fixer (templates produce the noisiest diffs). The realistic first "win" is still a root rector.php that is five lines instead of eighty:

<?php
// rector.php
return require __DIR__ . '/vendor/nowo-tech/php-quality-tools/config/rector.php';

Run vendor/bin/rector --version and vendor/bin/php-cs-fixer --version immediately after install so you know which binaries the package pulled in — those are the ones your editor plugin and CI must point at, not a globally installed copy.

Implementation Patterns

Pattern 1 — "thin root config that delegates". Keep your project root config files as the only place developers edit, and have them require the package config as a base, then layer project-specific overrides on top:

// .php-cs-fixer.dist.php
$config = require __DIR__ . '/vendor/nowo-tech/php-quality-tools/config/php-cs-fixer.php';

return $config
    ->setFinder(...)          // re-assert your finder; see Gotchas
    ->setRules(array_merge(
        $config->getRules(),
        ['final_class' => false],   // your exception wins
    ));

Package rules first, project rules second. array_merge gives last-wins semantics, which is what you want. Never invert it.

Pattern 2 — separate "check" and "fix" Composer scripts, now with a gate group. This remains the highest-leverage thing you can do, because it makes tooling discoverable without anyone reading the README:

"scripts": {
  "quality":       ["@quality:rector", "@quality:cs", "@quality:twig", "@quality:gates"],
  "quality:rector": "rector process --dry-run --no-progress-bar",
  "quality:cs":     "php-cs-fixer fix --dry-run --diff --using-cache=no",
  "quality:twig":   "twig-cs-fixer lint resources/views",
  "quality:gates":  ["@quality:docs", "@quality:svg", "@quality:budgets"],
  "quality:docs":   "check-doc-links --check-anchors",
  "quality:svg":    "check-no-inline-svg --templates=resources/views --scripts=resources/js",
  "quality:budgets":"check-asset-budgets",
  "fix":            ["rector process", "php-cs-fixer fix", "twig-cs-fixer fix resources/views"]
}

Developers type composer quality before pushing and composer fix when it fails. CI runs the same composer quality. Note the deliberate asymmetry: fix never touches the gates — they have no autofix, by design. If a gate fails you change the code (or move the icon to a component), you don't run a formatter.

Pattern 3 — path-scoped rollout with a committed allowlist. Rather than excluding noise forever, invert it: start with paths limited to app/ and tests/, expand directory by directory. Each PR is a reviewable diff and the config file doubles as a rollout checklist. Reserve withSkip / exclude for genuinely generated things: bootstrap/cache, storage/framework, _ide_helper.php, _ide_helper_models.php, and published vendor config you don't own.

Pattern 4 — Rector as a scheduled migration job, not a pre-commit hook. Rector rewrites code; a half-applied refactor must not land in a commit hook. Weekly CI job (or workflow_dispatch) opens a PR running composer fix, with the shared config version pinned. Style fixers (CS-Fixer, Twig) are safe in a pre-commit hook on staged files only. Gates are safe anywhere, including pre-push — they're fast and read-only, which makes composer quality:gates a good pre-push hook.

Pattern 5 — budgets as a config file, not CLI flags, and in PHP unless you mean it. check-asset-budgets reads config/quality/asset-budgets.{yaml,yml,json,php} and/or --budget=<entry>:<kind>=<size>:

<?php
// config/quality/asset-budgets.php  — no symfony/yaml needed for this form
return [
    'app'  => ['js' => '250kB', 'css' => '40kB', 'js_gzip' => '80kB'],
    'admin'=> ['js' => '1.5KiB'],
];

Choose the .php (or .json) variant unless you have deliberately installed symfony/yaml — it's only a suggestion of the package, so YAML works right up until a clean CI container builds from composer.lock without the suggester installed. Keep budgets in the file so they're reviewable in diffs; use --budget= only for one-off experiments.

Pattern 6 — gate ordering in CI reflects what the gate consumes. Anything reading the Vite manifest must run after npm run build in the same job, and the build must use the environment you care about. Docs and SVG gates read source only and can live in a cheap, early job that fails in seconds.

- run: npm ci && npm run build      # budgets read the manifest from here
- run: vendor/bin/check-asset-budgets
- run: vendor/bin/check-doc-links --check-anchors
- run: vendor/bin/check-no-inline-svg --templates=resources/views --scripts=resources/js

Pattern 7 — cache keyed on the lock file. PHP-CS-Fixer and Rector cache aggressively (the new gate binaries do not). In GitHub Actions:

- uses: actions/cache@v4
  with:
    path: |
      .php-cs-fixer.cache
      .rector.cache
    key: quality-${{ hashFiles('composer.lock') }}-${{ matrix.php }}

Include the PHP version in the key. Half the "the fixer stopped fixing anything" tickets in a repo using a shared config package are stale caches after a dependency bump.

Pattern 8 — one owner per file extension. If the app already uses Pint, decide explicitly: Pint owns app/ and tests/, the shared PHP-CS-Fixer config owns nothing (or vice versa), and Rector + Twig-CS-Fixer are non-overlapping. Two style engines on the same path means every branch conflict is a formatting conflict.

Pattern 9 — stale workers after a build. The new resources/make/php-worker-reset.mk snippet is the missing half of the asset story: after npm run build, long-lived PHP workers (Octane, queue workers, FPM with OPcache) can serve the old Vite entrypoint. Include it and parameterise the service names:

include vendor/nowo-tech/php-quality-tools/resources/make/php-worker-reset.mk
PHP_SERVICE ?= php
# optional second stack (e.g. E2E) only acts when it is up

It clears the cache, restarts the service(s), and waits for healthy — which means the target only works if your Compose service actually defines a healthcheck. Chain it after the frontend build, not before.

Pattern 10 — IDE wiring as a force multiplier. Point PhpStorm's PHP-CS-Fixer and Rector watchers at vendor/bin/* and at your root config files. Because the root configs delegate to the package, a package upgrade immediately changes IDE behaviour too — one source of truth instead of an editor profile that drifts.

Gotchas and Tips

Dependency pinning is still the real risk, not the code. The package pulls its own rector/rector, friendsofphp/php-cs-fixer, and Twig-CS-Fixer versions. If your app or larastan/pint already constrains those, Composer either conflicts outright or silently resolves outside the version the shipped configs were written against. Run composer why php-cs-fixer and composer show nowo-tech/php-quality-tools before assuming the configs match your binaries. Pin exactly ("1.6.0", not ^1.6) — a patch bump to a shared rule set can reformat the entire repository. v1.6.0 adds nothing to require, so the dependency graph is unchanged; the risk profile is identical to before.

symfony/yaml is suggested, not required. The release notes say the suggestion "also covers the asset budgets config". If you write config/quality/asset-budgets.yaml without the YAML parser installed, you get a runtime failure deep inside the gate rather than a helpful install hint. Either add symfony/yaml to your own require-dev explicitly, or use the .php/.json config form.

Asset budgets read a manifest that may not exist or may be stale. The gate measures the Vite production build. Running it before npm run build, against a manifest from a previous branch, or after a npm run dev will give you numbers that mean nothing (or an empty pass). Also: only entry + statically imported chunks count — lazy/dynamic chunks are excluded, so a budget can look comfortable while your actual first-load payload grows because someone moved code behind import(). Treat budgets as a floor on quality, not a proof of performance.

The new gates default to Symfony-shaped paths. check-no-inline-svg scans templates/ with .twig by default and assets/ with .ts; check-doc-links walks tracked Markdown. In a Laravel app that means the SVG gate scans nothing and passes — the most dangerous possible outcome. Always pass --templates=resources/views --template-ext=blade.php --scripts=resources/js --script-ext=ts, then sanity-check by temporarily introducing a <svg and confirming a FAIL with a line number. A gate that has never failed is a gate you haven't verified.

Inline SVG is often deliberate in Laravel. Blade partials with <svg> are a common icon strategy. Decide the policy up front: either move icons into components and exclude that directory via --allow=<path>, or accept that this gate is not for you. Note the allowlist semantics: a stale allow entry fails, so removing or renaming an icon file breaks CI until you prune the flag — good hygiene, but it will surprise you in a refactor PR.

check-no-inline-svg also greps front-end scripts for SVG markup strings (<svg, <path, <circle, <rect, <polyline, <polygon>), skipping *.test.*, *.spec.*, *.d.ts, node_modules/. A legitimate icon-definitions module will trip it. That's what --allow is for, not a || true.

check-doc-links scope. It checks relative links and images, skips fenced and inline code, handles /root-relative targets and nested-bracket badges ([![badge](img)](url)). --check-anchors is opt-in and matches GitHub heading slugs, HTML anchors and {#id} heading attributes — so anchors drift if your renderer isn't GitHub-flavoured (GitLab, MkDocs). Since it works from git ls-files, untracked files are the classic local-vs-CI discrepancy: it passes locally on a doc you haven't git added, or you think it passed when it never looked. Verify untracked-new-file behaviour once, deliberately.

required configs and __DIR__. Config files inside vendor/ that compute paths with __DIR__ resolve relative to the package directory, not your project. Symptom: exclusions that "don't work", or Twig-CS-Fixer suddenly linting something under vendor/. Always call setFinder() / paths() explicitly in your root config rather than trusting the base.

Risky rules and setRiskyAllowed. PHP-CS-Fixer silently ignores risky rules unless setRiskyAllowed(true) is set on the Config object you actually return. If you build a fresh Config in your root file instead of mutating the returned one, you drop the flag and a dozen rules stop applying with no error. This is the most common "shared config isn't working" bug.

Non-zero exit is correct behaviour — for every one of these tools. rector process --dry-run exits non-zero when changes are needed; the gate binaries emit OK / FAIL lines and exit non-zero on failure. Do not "fix" CI with || true — that turns the gate into decoration. To pass while burning down a backlog, scope paths, raise a budget, or add a justified allow entry, and let the diff show it.

Twig-CS-Fixer produces large whitespace diffs. Template formatting is not surgical — the first run on resources/views can touch hundreds of files. Land it as its own commit, on a quiet branch, and tell reviewers to skip the diff. Confirm the binary name (twig-cs-fixer vs. an upstream variant) with ls vendor/bin before writing scripts — doubly so now that vendor/bin holds three more entries and tab-completion lies.

PHP version skew changes the result. Nullable-type handling, readonly promotion, and Rector's withPhpVersion(...) target depend on the running PHP and the declared target. Linting under 8.3 while deploying to 8.1 generates syntax your runtime can't parse. Pin the CI matrix to your minimum supported version.

Debugging checklist, in order:

  1. vendor/bin/php-cs-fixer fix --dry-run --diff --using-cache=no -vvv — bypass the cache first, always.
  2. vendor/bin/php-cs-fixer describe <rule_name> — confirms the rule exists in this version and whether it's risky.
  3. vendor/bin/rector process <path> --dry-run with no cache (rm -rf .rector.cache) — caches survive config changes in some versions.
  4. vendor/bin/<gate> --help then a deliberate failing fixture — the new gates have no cache, so any failure is real; the usual cause is wrong paths (scanning nothing) or a stale allow entry.
  5. composer show nowo-tech/php-quality-tools — verify you're not accidentally on a dev-main resolution.
  6. Print the effective rules: $config->getRules() in a throwaway script. Half of all merge bugs are visible in one var_dump.
  7. composer why symfony/yaml — before believing a YAML budget config "doesn't exist".

Parallelism in constrained CI containers. PHP-CS-Fixer runs files in parallel and will exhaust memory on 2-core runners with a large repo. Cap it (setParallelConfig(ParallelConfigFactory::sequential()) or a small filesPerProcess) and set --memory-limit on Rector rather than disabling the gate.

Twig and Blade coexist. In a Laravel app, resources/views is mostly Blade. Twig-CS-Fixer pointed there will no-op or match the wrong files. Point it at the actual Twig directory, or skip the Twig tool entirely — and now apply the same reasoning to the SVG gate's template extension. Installing a tool "for completeness" just adds a binary nobody runs.

Adoption and maintenance risk, slightly reduced but still present. One apparent maintainer and forward-dated releases still argue for treating this as a convenience wrapper, not infrastructure. Two mitigating factors in v1.6.0: nothing was added to require (no new supply-chain surface beyond code already in the package), and the three gates are openly ported from an existing project's checks, with the package dogfooding them (make docs-links, part of its own release-check). Structural mitigation is unchanged and is the reason to keep root configs thin: because they're five-line delegates, removing the package is a five-minute change to inlined configs. If you exceed ~15 lines of overrides, you've outgrown the shared set — inline the config and drop the dependency.

Extension points you actually have. Your root config files (rules, finder, paths), the config/quality/asset-budgets.* file, gate CLI flags, Composer scripts, and CI workflow steps. There is no plugin or event system to hook. Don't look for one — override at the config-file boundary and keep the package unmodified in vendor/.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource