projectRootPath(); $schemaFiles = []; $this->collectFilterSchemas($root . '/pages', $schemaFiles); $this->collectFilterSchemas($root . '/modules', $schemaFiles); $this->assertNotEmpty($schemaFiles, 'No filter-schema.php files found — regex stale?'); $violations = []; foreach ($schemaFiles as $schemaFile) { $schema = @include $schemaFile; if (!is_array($schema)) { $violations[] = sprintf('%s — filter-schema.php did not return an array', $this->relative($schemaFile, $root)); continue; } $queryKeys = array_keys(is_array($schema['query'] ?? null) ? $schema['query'] : []); $toolbarFields = is_array($schema['toolbar'] ?? null) ? $schema['toolbar'] : []; // Schemas without a toolbar are for non-Grid endpoints (e.g. global // search data endpoint consumed by a custom JS dialog). Skip those — // they never participate in Grid.js URL sync. if ($toolbarFields === []) { continue; } $toolbarKeys = []; foreach ($toolbarFields as $field) { if (!is_array($field)) { continue; } $key = trim((string) ($field['key'] ?? '')); if ($key !== '') { $toolbarKeys[] = $key; } } $missing = array_diff($queryKeys, $toolbarKeys, self::FRAMEWORK_KEYS); if ($missing) { $violations[] = sprintf( "%s — query keys missing from toolbar: %s.\n Add them to the 'toolbar' section (type 'hidden' is fine) so Grid.js forwards them on refetch.", $this->relative($schemaFile, $root), implode(', ', $missing) ); } } $this->assertSame( [], $violations, "Filter-schema consistency violations:\n" . implode("\n\n", $violations) ); } /** * @param list &$out */ private function collectFilterSchemas(string $base, array &$out): void { if (!is_dir($base)) { return; } $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base, \FilesystemIterator::SKIP_DOTS)); /** @var \SplFileInfo $file */ foreach ($iterator as $file) { if ($file->isFile() && $file->getFilename() === 'filter-schema.php') { $out[] = $file->getPathname(); } } } private function relative(string $path, string $root): string { return str_replace($root . '/', '', $path); } }