Created
July 11, 2023 11:26
-
-
Save janklan/b43e37602fb79abe4f9d98669f847fe7 to your computer and use it in GitHub Desktop.
A Symfony command that looks for routes without declared placeholder requirements
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Command\Security; | |
use Symfony\Component\Console\Attribute\AsCommand; | |
use Symfony\Component\Console\Command\Command; | |
use Symfony\Component\Console\Input\InputInterface; | |
use Symfony\Component\Console\Output\OutputInterface; | |
use Symfony\Component\Console\Style\SymfonyStyle; | |
use Symfony\Component\Routing\RouterInterface; | |
#[AsCommand( | |
name: 'app:security:route-requirements', | |
description: 'Finds all routes where the placeholder requirements are not set', | |
)] | |
class RouteRequirementsCommand extends Command | |
{ | |
public function __construct(private readonly RouterInterface $router) | |
{ | |
parent::__construct(); | |
} | |
/** | |
* Every route with placeholders should define proper requirements so that we don't accept any value at all times. | |
* | |
* This command is part of the CI/CD pipeline to make sure if a new placeholder is added without requirements, | |
* the pipeline fails. | |
*/ | |
protected function execute(InputInterface $input, OutputInterface $output): int | |
{ | |
$io = new SymfonyStyle($input, $output); | |
$routeCollection = $this->router->getRouteCollection(); | |
$result = []; | |
foreach ($routeCollection->all() as $name => $route) { | |
// Skip what's not ours | |
if (null === $route->getDefault('_controller')) { | |
continue; | |
} | |
if (!str_starts_with($route->getDefault('_controller'), 'App') && !str_starts_with($route->getDefault('_controller'), 'Cognetiq')) { | |
continue; | |
} | |
// Find routes that define at least one {placeholder} | |
if (preg_match_all('/\{([^\}]+)\}/', $route->getPath(), $placeholders)) { | |
// Ignore requirements that are not relevant to that route (requirements can be set in yaml, or at the class level too) | |
$relevantRequirements = array_intersect_key($route->getRequirements(), array_flip($placeholders[1])); | |
$missingRequirements = array_diff_key(array_flip($placeholders[1]), $relevantRequirements); | |
// We don't care about this one | |
unset($missingRequirements['_format']); | |
if (!empty($missingRequirements)) { | |
$result[] = [$name, implode(', ', array_flip($missingRequirements))]; | |
} | |
} | |
} | |
ksort($result); | |
if (empty($result)) { | |
$io->success('All route placeholders requirements defined.'); | |
return Command::SUCCESS; | |
} | |
$io->error('Some routes have placeholders without explicit requirements.'); | |
$io->table( | |
['Route Name', 'Offending placeholder(s)'], | |
$result, | |
); | |
return Command::FAILURE; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment