Repository navigation
Conversation
Implements the issues grid requested in #2 via a generic, backend-extensible detection engine. IssueDetectorInterface::detect(Inspection) hands every check the HTTP status, all response headers, the raw body and the parsed DOM, so virtually anything can be tested on a page. - 15 built-in checks (HTTP status, X-Robots-Tag / meta noindex, title, meta description, H1, canonical, image alt, html lang, viewport, Open Graph, JSON-LD, mixed content) plus 4 backend-configurable checks with no code: element_content (CSS/XPath selector + optional attribute + JSON path + contains/equals/matches/exists/absent), element_exists, header, status_code - Admin Page resource (route + sample resource + per-page checks via a Symfony UX live form) and Issue grid with severity badges, filters and ignore/restore - HTTP fetcher, runner and fingerprint-based upsert persistence (ignores survive re-runs; missing issues are marked resolved) - setono:sylius-seo:detect-issues command (--channel, --page) - Doctrine migrations (MySQL + PostgreSQL), translations, config (scheme, base_url) - Unit + functional tests; PHPStan (max) and ECS clean
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## 2.x #6 +/- ##
=============================================
- Coverage 96.81% 63.69% -33.12%
- Complexity 264 606 +342
=============================================
Files 52 105 +53
Lines 816 2019 +1203
=============================================
+ Hits 790 1286 +496
- Misses 26 733 +707 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| # Admin routes for the SEO checks feature. | ||
| # | ||
| # Import this file from your application UNDER the admin prefix and only when the feature is | ||
| # enabled (`setono_sylius_seo.checks.enabled: true`), e.g. in config/routes/setono_sylius_seo_admin.yaml: | ||
| # | ||
| # setono_sylius_seo_admin: | ||
| # resource: "@SetonoSyliusSEOPlugin/config/routes/admin.yaml" | ||
| # prefix: '/%sylius_admin.path_name%' |
There was a problem hiding this comment.
Removed the comment block. Admin routes now follow the skeleton pattern — config/routes.yaml imports config/routes/admin.yaml under /admin, so consumers get them from the main routing import. (b84e085)
| <php> | ||
| <env name="APP_ENV" value="test"/> | ||
| <env name="SHELL_VERBOSITY" value="-1"/> | ||
| <env name="KERNEL_CLASS" value="Setono\SyliusSEOPlugin\Tests\Application\Kernel"/> |
There was a problem hiding this comment.
Usually this is defined in .env.test
There was a problem hiding this comment.
Removed it from phpunit.xml.dist. KERNEL_CLASS was already defined in tests/Application/.env.test, which now takes effect. (b84e085)
| @@ -0,0 +1,5 @@ | |||
| {% if data %} | |||
There was a problem hiding this comment.
I am pretty sure Sylius has a built-in grid field like this
There was a problem hiding this comment.
You are right — switched to the built-in @SyliusUi/grid/field/yes_no.html.twig and deleted the custom template. (b84e085)
| # In local development the shop runs on a non-standard port, so point the checker at it | ||
| # explicitly (the channel hostname alone has no port). Using the channel's "localhost" host | ||
| # means Sylius still resolves the right channel for the fetched page. | ||
| base_url: 'http://localhost:8099' |
There was a problem hiding this comment.
We should not have a base_url configuration parameter. We should use the built-in in the framework or create a compiler pass or something else in the test application that will discover the URL by running symfony status or something
There was a problem hiding this comment.
Removed the base_url/scheme config entirely. ChannelUrlGenerator now builds absolute URLs from the framework router request context (framework.router.default_uri), overriding the host with the channel hostname. The test app discovers the running URL from the SYMFONY_DEFAULT_ROUTE_URL env var that symfony serve injects (see tests/Application/config/packages/routing.yaml); functional tests point it at a refused port for determinism. (b84e085)
| @@ -0,0 +1,3 @@ | |||
| setono_sylius_seo_admin: | |||
There was a problem hiding this comment.
Use the pattern for routes that the plugin skeleton uses: https://github.com/Setono/SyliusPluginSkeleton/tree/2.2.x/config
There was a problem hiding this comment.
Adopted the skeleton pattern: the plugin config/routes.yaml imports config/routes/admin.yaml with prefix: /admin, and the separate setono_sylius_seo_admin.yaml import in the test app is gone. (b84e085)
|
|
||
| interface PageRepositoryInterface | ||
| { | ||
| public function findOneById(int $id): ?PageInterface; |
There was a problem hiding this comment.
Repositories should use the built-in Sylius interfaces an classes
There was a problem hiding this comment.
Done — PageRepositoryInterface/IssueRepositoryInterface now extend Sylius\Resource\Doctrine\Persistence\RepositoryInterface (generic, like the core repos), the concretes extend EntityRepository, and I dropped the custom findOneById() for the inherited find(). (b84e085)
| @@ -0,0 +1,55 @@ | |||
| <?php | |||
There was a problem hiding this comment.
Plugins should not carry migrations. The end user will run doctrine:diff instead
There was a problem hiding this comment.
Done — removed src/Migrations. The README now tells consumers to generate one with doctrine:migrations:diff. (b84e085)
|
|
||
| use Sylius\Bundle\UiBundle\Menu\Event\MenuBuilderEvent; | ||
|
|
||
| final class AdminMenuListener |
There was a problem hiding this comment.
Make this an event subscriber instead and put it in src/EventSubscriber
There was a problem hiding this comment.
Done — moved to src/EventSubscriber/AdminMenuSubscriber.php implementing EventSubscriberInterface. (b84e085)
| ->add('channel', ChannelChoiceType::class, [ | ||
| 'label' => 'sylius.ui.channel', | ||
| ]) | ||
| ->add('localeCode', TextType::class, [ |
There was a problem hiding this comment.
Bad UX that we need to put the locale code as free text
There was a problem hiding this comment.
Agreed. The locale is now a dropdown of the store enabled locales (via Sylius LocaleProviderInterface) with a "Use the channel default" placeholder, instead of free text. (b84e085)
| * Delegates URL resolution to the first registered page-type resolver that supports the page. | ||
| * Also exposes the list of available page types for the admin form. | ||
| */ | ||
| final class CompositeUrlResolver implements UrlResolverInterface |
There was a problem hiding this comment.
Use this library: https://github.com/Setono/composite-compiler-pass for composite services
There was a problem hiding this comment.
Done — CompositeUrlResolver now extends CompositeService and the resolvers are collected by a CompositeCompilerPass registered in the bundle, matching the data-mapper composites. (448e76e)
| * request to derive the host/scheme from. The path comes from the router; the authority comes | ||
| * from the configured base URL, or the channel hostname plus the configured scheme. | ||
| */ | ||
| final class ChannelUrlGenerator |
There was a problem hiding this comment.
Services should usually implement an interface, so plugin users can override/decorate the service easily
There was a problem hiding this comment.
Done — extracted ChannelUrlGeneratorInterface; ChannelUrlGenerator implements it and the resolvers depend on the interface, so it can be decorated/overridden. The other checker services already expose interfaces (CheckRunnerInterface, PageFetcherInterface, IssuePersisterInterface, UrlResolverInterface, DetectorRegistryInterface). (448e76e)
| * A stable, unique machine identifier for this check (e.g. "title_length"). Used to assign the | ||
| * check to a page and to group/filter the resulting issues. | ||
| */ | ||
| public function getCode(): string; |
There was a problem hiding this comment.
Should we have an PHP attribute where you can define the code instead?
There was a problem hiding this comment.
I kept getCode() as a method for now — it mirrors the Sylius convention (grid field/filter types expose getName()/getType() the same way) and keeps the registry reflection-free (the tagged iterator plus getCode() is enough to index by code). An attribute such as #[AsIssueDetector("title_length")] would be a nice declarative alternative but needs a compiler pass to read it. Happy to switch if you prefer the attribute approach.
|
|
||
| namespace Setono\SyliusSEOPlugin\Checker\Detector; | ||
|
|
||
| interface DetectorRegistryInterface |
There was a problem hiding this comment.
Should registries be traversable?
There was a problem hiding this comment.
The registry exposes all() returning a code => detector map (what the admin form needs to render the catalog) plus get(code)/has(code) for the runner. I left it non-Traversable since callers rely on the code keys and there is no iterate-all use case beyond all(). I can make it IteratorAggregate yielding code => detector if you would rather drop all() in favour of iteration — let me know.
- Register Page/Issue as Sylius resources via AbstractResourceExtension + AbstractResourceBundle (registerResources) instead of hand-rolled sylius_resource config - Repositories extend Sylius's generic RepositoryInterface and use the inherited find() - Drop the shipped migration; consumers generate one with doctrine:migrations:diff - Convert the admin menu listener to an EventSubscriber - Remove the base_url config option; build absolute URLs from the framework router request context (framework.router.default_uri), discovered from `symfony serve` in the test app - Follow the plugin skeleton routing pattern: config/routes.yaml imports the admin routes under /admin - Restrict the page locale field to the store's enabled locales instead of free-text - Use the built-in @SyliusUi/grid/field/yes_no.html.twig grid field - Move KERNEL_CLASS from phpunit.xml.dist to .env.test
…face) - Collect the page URL resolvers with setono/composite-compiler-pass (CompositeUrlResolver now extends CompositeService), matching the data-mapper composites - Extract ChannelUrlGeneratorInterface so the URL generator can be decorated/overridden; the resolvers depend on the interface
|
Thanks for the thorough review! Pushed two commits (b84e085, 448e76e) and replied to every inline comment. Entities as Sylius resourcesSwitched from the hand-rolled
One judgement call worth flagging: Status of the comments
Verified green: PHPStan max, ECS, 297 tests (incl. the functional command test), and in the browser — the Pages/Issues grids, the live check form with the restricted locale dropdown, running checks, and the ignore lifecycle all work. |
Summary
Adds an admin SEO checks feature (resolves #2): define the pages you want to monitor, fetch them over HTTP, and run them through a battery of generic checks that produce an issues grid with severities and an ignore action.
The core is a generic detection engine so virtually anything can be tested —
IssueDetectorInterface::detect(Inspection)hands every check the HTTP status, all response headers, the raw body and the parsed DOM.What's included
element_content(CSS or XPath selector + optional attribute + optional JSON path + contains/equals/matches/exists/absent),element_exists,header,status_code.Pageresource (route + representative sample resource + per-page check selection via a Symfony UX live form) and anIssuegrid (severity badges, filters, ignore/restore). Issues upsert by a stable fingerprint, so ignores survive re-runs and resolved issues are tracked.setono:sylius-seo:detect-issues(--channel,--page).scheme,base_url).Extending
Implement
IssueDetectorInterface(autoconfigured) to add a check — it becomes selectable on every page. ImplementConfigurableIssueDetectorInterfacefor admin-editable configuration.Tests & verification
Setup for consumers
Import
@SetonoSyliusSEOPlugin/config/routes/admin.yamlunder the admin prefix and run the migrations. See the README's "SEO checks" section.