first commit

This commit is contained in:
2024-09-06 13:32:15 -04:00
commit 700e8a2948
2013 changed files with 447887 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\Cache\FactoriesCache;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Config\BaseConfig;
use Config\Optimize;
use Kint\Kint;
/**
* Check the Config values.
*
* @see \CodeIgniter\Commands\Utilities\ConfigCheckTest
*/
final class ConfigCheck extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'config:check';
/**
* The Command's short description
*
* @var string
*/
protected $description = 'Check your Config values.';
/**
* The Command's usage
*
* @var string
*/
protected $usage = 'config:check <classname>';
/**
* The Command's arguments
*
* @var array<string, string>
*/
protected $arguments = [
'classname' => 'The config classname to check. Short classname or FQCN.',
];
/**
* The Command's options
*
* @var array<string, string>
*/
protected $options = [];
/**
* {@inheritDoc}
*/
public function run(array $params)
{
if (! isset($params[0])) {
CLI::error('You must specify a Config classname.');
CLI::write(' Usage: ' . $this->usage);
CLI::write('Example: config:check App');
CLI::write(' config:check \'CodeIgniter\Shield\Config\Auth\'');
return EXIT_ERROR;
}
/** @var class-string<BaseConfig> $class */
$class = $params[0];
// Load Config cache if it is enabled.
$configCacheEnabled = class_exists(Optimize::class)
&& (new Optimize())->configCacheEnabled;
if ($configCacheEnabled) {
$factoriesCache = new FactoriesCache();
$factoriesCache->load('config');
}
$config = config($class);
if ($config === null) {
CLI::error('No such Config class: ' . $class);
return EXIT_ERROR;
}
if (defined('KINT_DIR') && Kint::$enabled_mode !== false) {
CLI::write($this->getKintD($config));
} else {
CLI::write(
CLI::color($this->getVarDump($config), 'cyan')
);
}
CLI::newLine();
$state = CLI::color($configCacheEnabled ? 'Enabled' : 'Disabled', 'green');
CLI::write('Config Caching: ' . $state);
return EXIT_SUCCESS;
}
/**
* Gets object dump by Kint d()
*/
private function getKintD(object $config): string
{
ob_start();
d($config);
$output = ob_get_clean();
$output = trim($output);
$lines = explode("\n", $output);
array_splice($lines, 0, 3);
array_splice($lines, -3);
return implode("\n", $lines);
}
/**
* Gets object dump by var_dump()
*/
private function getVarDump(object $config): string
{
ob_start();
var_dump($config);
$output = ob_get_clean();
return preg_replace(
'!.*system/Commands/Utilities/ConfigCheck.php.*\n!u',
'',
$output
);
}
}
+157
View File
@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Config\DotEnv;
/**
* Command to display the current environment,
* or set a new one in the `.env` file.
*/
final class Environment extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'env';
/**
* The Command's short description
*
* @var string
*/
protected $description = 'Retrieves the current environment, or set a new one.';
/**
* The Command's usage
*
* @var string
*/
protected $usage = 'env [<environment>]';
/**
* The Command's arguments
*
* @var array<string, string>
*/
protected $arguments = [
'environment' => '[Optional] The new environment to set. If none is provided, this will print the current environment.',
];
/**
* The Command's options
*
* @var array<string, string>
*/
protected $options = [];
/**
* Allowed values for environment. `testing` is excluded
* since spark won't work on it.
*
* @var array<int, string>
*/
private static array $knownTypes = [
'production',
'development',
];
/**
* {@inheritDoc}
*/
public function run(array $params)
{
if ($params === []) {
CLI::write(sprintf('Your environment is currently set as %s.', CLI::color($_SERVER['CI_ENVIRONMENT'] ?? ENVIRONMENT, 'green')));
CLI::newLine();
return;
}
$env = strtolower(array_shift($params));
if ($env === 'testing') {
CLI::error('The "testing" environment is reserved for PHPUnit testing.', 'light_gray', 'red');
CLI::error('You will not be able to run spark under a "testing" environment.', 'light_gray', 'red');
CLI::newLine();
return;
}
if (! in_array($env, self::$knownTypes, true)) {
CLI::error(sprintf('Invalid environment type "%s". Expected one of "%s".', $env, implode('" and "', self::$knownTypes)), 'light_gray', 'red');
CLI::newLine();
return;
}
if (! $this->writeNewEnvironmentToEnvFile($env)) {
CLI::error('Error in writing new environment to .env file.', 'light_gray', 'red');
CLI::newLine();
return;
}
// force DotEnv to reload the new environment
// however we cannot redefine the ENVIRONMENT constant
putenv('CI_ENVIRONMENT');
unset($_ENV['CI_ENVIRONMENT'], $_SERVER['CI_ENVIRONMENT']);
(new DotEnv(ROOTPATH))->load();
CLI::write(sprintf('Environment is successfully changed to "%s".', $env), 'green');
CLI::write('The ENVIRONMENT constant will be changed in the next script execution.');
CLI::newLine();
}
/**
* @see https://regex101.com/r/4sSORp/1 for the regex in action
*/
private function writeNewEnvironmentToEnvFile(string $newEnv): bool
{
$baseEnv = ROOTPATH . 'env';
$envFile = ROOTPATH . '.env';
if (! is_file($envFile)) {
if (! is_file($baseEnv)) {
CLI::write('Both default shipped `env` file and custom `.env` are missing.', 'yellow');
CLI::write('It is impossible to write the new environment type.', 'yellow');
CLI::newLine();
return false;
}
copy($baseEnv, $envFile);
}
$pattern = preg_quote($_SERVER['CI_ENVIRONMENT'] ?? ENVIRONMENT, '/');
$pattern = sprintf('/^[#\s]*CI_ENVIRONMENT[=\s]+%s$/m', $pattern);
return file_put_contents(
$envFile,
preg_replace($pattern, "\nCI_ENVIRONMENT = {$newEnv}", file_get_contents($envFile), -1, $count)
) !== false && $count > 0;
}
}
+155
View File
@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Commands\Utilities\Routes\FilterCollector;
/**
* Check filters for a route.
*/
class FilterCheck extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'filter:check';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Check filters for a route.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'filter:check <HTTP method> <route>';
/**
* the Command's Arguments
*
* @var array<string, string>
*/
protected $arguments = [
'method' => 'The HTTP method. GET, POST, PUT, etc.',
'route' => 'The route (URI path) to check filters.',
];
/**
* the Command's Options
*
* @var array<string, string>
*/
protected $options = [];
/**
* @return int exit code
*/
public function run(array $params)
{
$tbody = [];
if (! isset($params[0], $params[1])) {
CLI::error('You must specify a HTTP verb and a route.');
CLI::write(' Usage: ' . $this->usage);
CLI::write('Example: filter:check GET /');
CLI::write(' filter:check PUT products/1');
return EXIT_ERROR;
}
$method = $params[0];
$route = $params[1];
// Load Routes
service('routes')->loadRoutes();
$filterCollector = new FilterCollector();
$filters = $filterCollector->get($method, $route);
// PageNotFoundException
if ($filters['before'] === ['<unknown>']) {
CLI::error(
"Can't find a route: " .
CLI::color(
'"' . strtoupper($method) . ' ' . $route . '"',
'black',
'light_gray'
),
);
return EXIT_ERROR;
}
$filters = $this->addRequiredFilters($filterCollector, $filters);
$tbody[] = [
strtoupper($method),
$route,
implode(' ', $filters['before']),
implode(' ', $filters['after']),
];
$thead = [
'Method',
'Route',
'Before Filters',
'After Filters',
];
CLI::table($tbody, $thead);
return EXIT_SUCCESS;
}
private function addRequiredFilters(FilterCollector $filterCollector, array $filters): array
{
$output = [];
$required = $filterCollector->getRequiredFilters();
$colored = [];
foreach ($required['before'] as $filter) {
$filter = CLI::color($filter, 'yellow');
$colored[] = $filter;
}
$output['before'] = array_merge($colored, $filters['before']);
$colored = [];
foreach ($required['after'] as $filter) {
$filter = CLI::color($filter, 'yellow');
$colored[] = $filter;
}
$output['after'] = array_merge($filters['after'], $colored);
return $output;
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Autoload;
/**
* Lists namespaces set in Config\Autoload with their
* full server path. Helps you to verify that you have
* the namespaces setup correctly.
*
* @see \CodeIgniter\Commands\Utilities\NamespacesTest
*/
class Namespaces extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'namespaces';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Verifies your namespaces are setup correctly.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'namespaces';
/**
* the Command's Arguments
*
* @var array<string, string>
*/
protected $arguments = [];
/**
* the Command's Options
*
* @var array<string, string>
*/
protected $options = [
'-c' => 'Show only CodeIgniter config namespaces.',
'-r' => 'Show raw path strings.',
'-m' => 'Specify max length of the path strings to output. Default: 60.',
];
/**
* Displays the help for the spark cli script itself.
*/
public function run(array $params)
{
$params['m'] = (int) ($params['m'] ?? 60);
$tbody = array_key_exists('c', $params) ? $this->outputCINamespaces($params) : $this->outputAllNamespaces($params);
$thead = [
'Namespace',
'Path',
'Found?',
];
CLI::table($tbody, $thead);
}
private function outputAllNamespaces(array $params): array
{
$maxLength = $params['m'];
$autoloader = service('autoloader');
$tbody = [];
foreach ($autoloader->getNamespace() as $ns => $paths) {
foreach ($paths as $path) {
if (array_key_exists('r', $params)) {
$pathOutput = $this->truncate($path, $maxLength);
} else {
$pathOutput = $this->truncate(clean_path($path), $maxLength);
}
$tbody[] = [
$ns,
$pathOutput,
is_dir($path) ? 'Yes' : 'MISSING',
];
}
}
return $tbody;
}
private function truncate(string $string, int $max): string
{
$length = strlen($string);
if ($length > $max) {
return substr($string, 0, $max - 3) . '...';
}
return $string;
}
private function outputCINamespaces(array $params): array
{
$maxLength = $params['m'];
$config = new Autoload();
$tbody = [];
foreach ($config->psr4 as $ns => $paths) {
foreach ((array) $paths as $path) {
if (array_key_exists('r', $params)) {
$pathOutput = $this->truncate($path, $maxLength);
} else {
$pathOutput = $this->truncate(clean_path($path), $maxLength);
}
$path = realpath($path) ?: $path;
$tbody[] = [
$ns,
$pathOutput,
is_dir($path) ? 'Yes' : 'MISSING',
];
}
}
return $tbody;
}
}
+149
View File
@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\Autoloader\FileLocator;
use CodeIgniter\Autoloader\FileLocatorCached;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Publisher\Publisher;
use RuntimeException;
/**
* Optimize for production.
*/
final class Optimize extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'optimize';
/**
* The Command's short description
*
* @var string
*/
protected $description = 'Optimize for production.';
/**
* The Command's usage
*
* @var string
*/
protected $usage = 'optimize';
/**
* {@inheritDoc}
*/
public function run(array $params)
{
try {
$this->enableCaching();
$this->clearCache();
$this->removeDevPackages();
} catch (RuntimeException) {
CLI::error('The "spark optimize" failed.');
return EXIT_ERROR;
}
return EXIT_SUCCESS;
}
private function clearCache(): void
{
$locator = new FileLocatorCached(new FileLocator(service('autoloader')));
$locator->deleteCache();
CLI::write('Removed FileLocatorCache.', 'green');
$cache = WRITEPATH . 'cache/FactoriesCache_config';
$this->removeFile($cache);
}
private function removeFile(string $cache): void
{
if (is_file($cache)) {
$result = unlink($cache);
if ($result) {
CLI::write('Removed "' . clean_path($cache) . '".', 'green');
return;
}
CLI::error('Error in removing file: ' . clean_path($cache));
throw new RuntimeException(__METHOD__);
}
}
private function enableCaching(): void
{
$publisher = new Publisher(APPPATH, APPPATH);
$config = APPPATH . 'Config/Optimize.php';
$result = $publisher->replace(
$config,
[
'public bool $configCacheEnabled = false;' => 'public bool $configCacheEnabled = true;',
'public bool $locatorCacheEnabled = false;' => 'public bool $locatorCacheEnabled = true;',
]
);
if ($result) {
CLI::write(
'Config Caching and FileLocator Caching are enabled in "app/Config/Optimize.php".',
'green'
);
return;
}
CLI::error('Error in updating file: ' . clean_path($config));
throw new RuntimeException(__METHOD__);
}
private function removeDevPackages(): void
{
if (! defined('VENDORPATH')) {
return;
}
chdir(ROOTPATH);
passthru('composer install --no-dev', $status);
if ($status === 0) {
CLI::write('Removed Composer dev packages.', 'green');
return;
}
CLI::error('Error in removing Composer dev packages.');
throw new RuntimeException(__METHOD__);
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\Security\CheckPhpIni;
/**
* Check php.ini values.
*/
final class PhpIniCheck extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'phpini:check';
/**
* The Command's short description
*
* @var string
*/
protected $description = 'Check your php.ini values.';
/**
* The Command's usage
*
* @var string
*/
protected $usage = 'phpini:check';
/**
* The Command's arguments
*
* @var array<string, string>
*/
protected $arguments = [
];
/**
* The Command's options
*
* @var array<string, string>
*/
protected $options = [];
/**
* {@inheritDoc}
*/
public function run(array $params)
{
CheckPhpIni::run();
return EXIT_SUCCESS;
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Publisher\Publisher;
/**
* Discovers all Publisher classes from the "Publishers/" directory
* across namespaces. Executes `publish()` from each instance, parsing
* each result.
*/
class Publish extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'publish';
/**
* The Command's short description
*
* @var string
*/
protected $description = 'Discovers and executes all predefined Publisher classes.';
/**
* The Command's usage
*
* @var string
*/
protected $usage = 'publish [<directory>]';
/**
* The Command's arguments
*
* @var array<string, string>
*/
protected $arguments = [
'directory' => '[Optional] The directory to scan within each namespace. Default: "Publishers".',
];
/**
* the Command's Options
*
* @var array<string, string>
*/
protected $options = [];
/**
* Displays the help for the spark cli script itself.
*/
public function run(array $params)
{
$directory = array_shift($params) ?? 'Publishers';
if ([] === $publishers = Publisher::discover($directory)) {
CLI::write(lang('Publisher.publishMissing', [$directory]));
return;
}
foreach ($publishers as $publisher) {
if ($publisher->publish()) {
CLI::write(lang('Publisher.publishSuccess', [
$publisher::class,
count($publisher->getPublished()),
$publisher->getDestination(),
]), 'green');
} else {
CLI::error(lang('Publisher.publishFailure', [
$publisher::class,
$publisher->getDestination(),
]), 'light_gray', 'red');
foreach ($publisher->getErrors() as $file => $exception) {
CLI::write($file);
CLI::error($exception->getMessage());
CLI::newLine();
}
}
}
}
}
+222
View File
@@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Commands\Utilities\Routes\AutoRouteCollector;
use CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\AutoRouteCollector as AutoRouteCollectorImproved;
use CodeIgniter\Commands\Utilities\Routes\FilterCollector;
use CodeIgniter\Commands\Utilities\Routes\SampleURIGenerator;
use CodeIgniter\Router\DefinedRouteCollector;
use CodeIgniter\Router\Router;
use Config\Feature;
use Config\Routing;
/**
* Lists all the routes. This will include any Routes files
* that can be discovered, and will include routes that are not defined
* in routes files, but are instead discovered through auto-routing.
*/
class Routes extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'routes';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Displays all routes.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'routes';
/**
* the Command's Arguments
*
* @var array<string, string>
*/
protected $arguments = [];
/**
* the Command's Options
*
* @var array<string, string>
*/
protected $options = [
'-h' => 'Sort by Handler.',
'--host' => 'Specify hostname in request URI.',
];
/**
* Displays the help for the spark cli script itself.
*/
public function run(array $params)
{
$sortByHandler = array_key_exists('h', $params);
$host = $params['host'] ?? null;
// Set HTTP_HOST
if ($host) {
$request = service('request');
$_SERVER = $request->getServer();
$_SERVER['HTTP_HOST'] = $host;
$request->setGlobal('server', $_SERVER);
}
$collection = service('routes')->loadRoutes();
// Reset HTTP_HOST
if ($host) {
unset($_SERVER['HTTP_HOST']);
}
$methods = Router::HTTP_METHODS;
$tbody = [];
$uriGenerator = new SampleURIGenerator();
$filterCollector = new FilterCollector();
$definedRouteCollector = new DefinedRouteCollector($collection);
foreach ($definedRouteCollector->collect() as $route) {
$sampleUri = $uriGenerator->get($route['route']);
$filters = $filterCollector->get($route['method'], $sampleUri);
$routeName = ($route['route'] === $route['name']) ? '»' : $route['name'];
$tbody[] = [
strtoupper($route['method']),
$route['route'],
$routeName,
$route['handler'],
implode(' ', array_map(class_basename(...), $filters['before'])),
implode(' ', array_map(class_basename(...), $filters['after'])),
];
}
if ($collection->shouldAutoRoute()) {
$autoRoutesImproved = config(Feature::class)->autoRoutesImproved ?? false;
if ($autoRoutesImproved) {
$autoRouteCollector = new AutoRouteCollectorImproved(
$collection->getDefaultNamespace(),
$collection->getDefaultController(),
$collection->getDefaultMethod(),
$methods,
$collection->getRegisteredControllers('*')
);
$autoRoutes = $autoRouteCollector->get();
// Check for Module Routes.
if ($routingConfig = config(Routing::class)) {
foreach ($routingConfig->moduleRoutes as $uri => $namespace) {
$autoRouteCollector = new AutoRouteCollectorImproved(
$namespace,
$collection->getDefaultController(),
$collection->getDefaultMethod(),
$methods,
$collection->getRegisteredControllers('*'),
$uri
);
$autoRoutes = [...$autoRoutes, ...$autoRouteCollector->get()];
}
}
} else {
$autoRouteCollector = new AutoRouteCollector(
$collection->getDefaultNamespace(),
$collection->getDefaultController(),
$collection->getDefaultMethod()
);
$autoRoutes = $autoRouteCollector->get();
foreach ($autoRoutes as &$routes) {
// There is no `AUTO` method, but it is intentional not to get route filters.
$filters = $filterCollector->get('AUTO', $uriGenerator->get($routes[1]));
$routes[] = implode(' ', array_map(class_basename(...), $filters['before']));
$routes[] = implode(' ', array_map(class_basename(...), $filters['after']));
}
}
$tbody = [...$tbody, ...$autoRoutes];
}
$thead = [
'Method',
'Route',
'Name',
$sortByHandler ? 'Handler ↓' : 'Handler',
'Before Filters',
'After Filters',
];
// Sort by Handler.
if ($sortByHandler) {
usort($tbody, static fn ($handler1, $handler2) => strcmp($handler1[3], $handler2[3]));
}
if ($host) {
CLI::write('Host: ' . $host);
}
CLI::table($tbody, $thead);
$this->showRequiredFilters();
}
private function showRequiredFilters(): void
{
$filterCollector = new FilterCollector();
$required = $filterCollector->getRequiredFilters();
$filters = [];
foreach ($required['before'] as $filter) {
$filters[] = CLI::color($filter, 'yellow');
}
CLI::write('Required Before Filters: ' . implode(', ', $filters));
$filters = [];
foreach ($required['after'] as $filter) {
$filters[] = CLI::color($filter, 'yellow');
}
CLI::write(' Required After Filters: ' . implode(', ', $filters));
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities\Routes;
/**
* Collects data for auto route listing.
*
* @see \CodeIgniter\Commands\Utilities\Routes\AutoRouteCollectorTest
*/
final class AutoRouteCollector
{
/**
* @param string $namespace namespace to search
*/
public function __construct(private readonly string $namespace, private readonly string $defaultController, private readonly string $defaultMethod)
{
}
/**
* @return list<list<string>>
*/
public function get(): array
{
$finder = new ControllerFinder($this->namespace);
$reader = new ControllerMethodReader($this->namespace);
$tbody = [];
foreach ($finder->find() as $class) {
$output = $reader->read(
$class,
$this->defaultController,
$this->defaultMethod
);
foreach ($output as $item) {
$tbody[] = [
'auto',
$item['route'],
'',
$item['handler'],
];
}
}
return $tbody;
}
}
@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved;
use CodeIgniter\Commands\Utilities\Routes\ControllerFinder;
use CodeIgniter\Commands\Utilities\Routes\FilterCollector;
/**
* Collects data for Auto Routing Improved.
*
* @see \CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\AutoRouteCollectorTest
*/
final class AutoRouteCollector
{
/**
* @param string $namespace namespace to search
* @param list<class-string> $protectedControllers List of controllers in Defined
* Routes that should not be accessed via Auto-Routing.
* @param list<string> $httpMethods
* @param string $prefix URI prefix for Module Routing
*/
public function __construct(
private readonly string $namespace,
private readonly string $defaultController,
private readonly string $defaultMethod,
private readonly array $httpMethods,
private readonly array $protectedControllers,
private string $prefix = ''
) {
}
/**
* @return list<list<string>>
*/
public function get(): array
{
$finder = new ControllerFinder($this->namespace);
$reader = new ControllerMethodReader($this->namespace, $this->httpMethods);
$tbody = [];
foreach ($finder->find() as $class) {
// Exclude controllers in Defined Routes.
if (in_array('\\' . $class, $this->protectedControllers, true)) {
continue;
}
$routes = $reader->read(
$class,
$this->defaultController,
$this->defaultMethod
);
if ($routes === []) {
continue;
}
$routes = $this->addFilters($routes);
foreach ($routes as $item) {
$route = $item['route'] . $item['route_params'];
// For module routing
if ($this->prefix !== '' && $route === '/') {
$route = $this->prefix;
} elseif ($this->prefix !== '') {
$route = $this->prefix . '/' . $route;
}
$tbody[] = [
strtoupper($item['method']) . '(auto)',
$route,
'',
$item['handler'],
$item['before'],
$item['after'],
];
}
}
return $tbody;
}
/**
* Adding Filters
*
* @param list<array<string, array|string>> $routes
*
* @return list<array<string, array|string>>
*/
private function addFilters($routes)
{
$filterCollector = new FilterCollector(true);
foreach ($routes as &$route) {
$routePath = $route['route'];
// For module routing
if ($this->prefix !== '' && $route === '/') {
$routePath = $this->prefix;
} elseif ($this->prefix !== '') {
$routePath = $this->prefix . '/' . $routePath;
}
// Search filters for the URI with all params
$sampleUri = $this->generateSampleUri($route);
$filtersLongest = $filterCollector->get($route['method'], $routePath . $sampleUri);
// Search filters for the URI without optional params
$sampleUri = $this->generateSampleUri($route, false);
$filtersShortest = $filterCollector->get($route['method'], $routePath . $sampleUri);
// Get common array elements
$filters['before'] = array_intersect($filtersLongest['before'], $filtersShortest['before']);
$filters['after'] = array_intersect($filtersLongest['after'], $filtersShortest['after']);
$route['before'] = implode(' ', array_map(class_basename(...), $filters['before']));
$route['after'] = implode(' ', array_map(class_basename(...), $filters['after']));
}
return $routes;
}
private function generateSampleUri(array $route, bool $longest = true): string
{
$sampleUri = '';
if (isset($route['params'])) {
$i = 1;
foreach ($route['params'] as $required) {
if ($longest && ! $required) {
$sampleUri .= '/' . $i++;
}
}
}
return $sampleUri;
}
}
@@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved;
use Config\Routing;
use ReflectionClass;
use ReflectionMethod;
/**
* Reads a controller and returns a list of auto route listing.
*
* @see \CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\ControllerMethodReaderTest
*/
final class ControllerMethodReader
{
private readonly bool $translateURIDashes;
private readonly bool $translateUriToCamelCase;
/**
* @param string $namespace the default namespace
* @param list<string> $httpMethods
*/
public function __construct(
private readonly string $namespace,
private readonly array $httpMethods
) {
$config = config(Routing::class);
$this->translateURIDashes = $config->translateURIDashes;
$this->translateUriToCamelCase = $config->translateUriToCamelCase;
}
/**
* Returns found route info in the controller.
*
* @param class-string $class
*
* @return list<array<string, array|string>>
*/
public function read(string $class, string $defaultController = 'Home', string $defaultMethod = 'index'): array
{
$reflection = new ReflectionClass($class);
if ($reflection->isAbstract()) {
return [];
}
$classname = $reflection->getName();
$classShortname = $reflection->getShortName();
$output = [];
$classInUri = $this->convertClassNameToUri($classname);
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$methodName = $method->getName();
foreach ($this->httpMethods as $httpVerb) {
if (str_starts_with($methodName, strtolower($httpVerb))) {
// Remove HTTP verb prefix.
$methodInUri = $this->convertMethodNameToUri($httpVerb, $methodName);
// Check if it is the default method.
if ($methodInUri === $defaultMethod) {
$routeForDefaultController = $this->getRouteForDefaultController(
$classShortname,
$defaultController,
$classInUri,
$classname,
$methodName,
$httpVerb,
$method
);
if ($routeForDefaultController !== []) {
// The controller is the default controller. It only
// has a route for the default method. Other methods
// will not be routed even if they exist.
$output = [...$output, ...$routeForDefaultController];
continue;
}
[$params, $routeParams] = $this->getParameters($method);
// Route for the default method.
$output[] = [
'method' => $httpVerb,
'route' => $classInUri,
'route_params' => $routeParams,
'handler' => '\\' . $classname . '::' . $methodName,
'params' => $params,
];
continue;
}
$route = $classInUri . '/' . $methodInUri;
[$params, $routeParams] = $this->getParameters($method);
// If it is the default controller, the method will not be
// routed.
if ($classShortname === $defaultController) {
$route = 'x ' . $route;
}
$output[] = [
'method' => $httpVerb,
'route' => $route,
'route_params' => $routeParams,
'handler' => '\\' . $classname . '::' . $methodName,
'params' => $params,
];
}
}
}
return $output;
}
private function getParameters(ReflectionMethod $method): array
{
$params = [];
$routeParams = '';
$refParams = $method->getParameters();
foreach ($refParams as $param) {
$required = true;
if ($param->isOptional()) {
$required = false;
$routeParams .= '[/..]';
} else {
$routeParams .= '/..';
}
// [variable_name => required?]
$params[$param->getName()] = $required;
}
return [$params, $routeParams];
}
/**
* @param class-string $classname
*
* @return string URI path part from the folder(s) and controller
*/
private function convertClassNameToUri(string $classname): string
{
// remove the namespace
$pattern = '/' . preg_quote($this->namespace, '/') . '/';
$class = ltrim(preg_replace($pattern, '', $classname), '\\');
$classParts = explode('\\', $class);
$classPath = '';
foreach ($classParts as $part) {
// make the first letter lowercase, because auto routing makes
// the URI path's first letter uppercase and search the controller
$classPath .= lcfirst($part) . '/';
}
$classUri = rtrim($classPath, '/');
return $this->translateToUri($classUri);
}
/**
* @return string URI path part from the method
*/
private function convertMethodNameToUri(string $httpVerb, string $methodName): string
{
$methodUri = lcfirst(substr($methodName, strlen($httpVerb)));
return $this->translateToUri($methodUri);
}
/**
* @param string $string classname or method name
*/
private function translateToUri(string $string): string
{
if ($this->translateUriToCamelCase) {
$string = strtolower(
preg_replace('/([a-z\d])([A-Z])/', '$1-$2', $string)
);
} elseif ($this->translateURIDashes) {
$string = str_replace('_', '-', $string);
}
return $string;
}
/**
* Gets a route for the default controller.
*
* @return list<array>
*/
private function getRouteForDefaultController(
string $classShortname,
string $defaultController,
string $uriByClass,
string $classname,
string $methodName,
string $httpVerb,
ReflectionMethod $method
): array {
$output = [];
if ($classShortname === $defaultController) {
$pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#';
$routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/');
$routeWithoutController = $routeWithoutController ?: '/';
[$params, $routeParams] = $this->getParameters($method);
if ($routeWithoutController === '/' && $routeParams !== '') {
$routeWithoutController = '';
}
$output[] = [
'method' => $httpVerb,
'route' => $routeWithoutController,
'route_params' => $routeParams,
'handler' => '\\' . $classname . '::' . $methodName,
'params' => $params,
];
}
return $output;
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities\Routes;
use CodeIgniter\Autoloader\FileLocatorInterface;
/**
* Finds all controllers in a namespace for auto route listing.
*
* @see \CodeIgniter\Commands\Utilities\Routes\ControllerFinderTest
*/
final class ControllerFinder
{
private readonly FileLocatorInterface $locator;
/**
* @param string $namespace namespace to search
*/
public function __construct(
private readonly string $namespace
) {
$this->locator = service('locator');
}
/**
* @return list<class-string>
*/
public function find(): array
{
$nsArray = explode('\\', trim($this->namespace, '\\'));
$count = count($nsArray);
$ns = '';
$files = [];
for ($i = 0; $i < $count; $i++) {
$ns .= '\\' . array_shift($nsArray);
$path = implode('\\', $nsArray);
$files = $this->locator->listNamespaceFiles($ns, $path);
if ($files !== []) {
break;
}
}
$classes = [];
foreach ($files as $file) {
if (\is_file($file)) {
$classnameOrEmpty = $this->locator->getClassname($file);
if ($classnameOrEmpty !== '') {
/** @var class-string $classname */
$classname = $classnameOrEmpty;
$classes[] = $classname;
}
}
}
return $classes;
}
}
@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities\Routes;
use ReflectionClass;
use ReflectionMethod;
/**
* Reads a controller and returns a list of auto route listing.
*
* @see \CodeIgniter\Commands\Utilities\Routes\ControllerMethodReaderTest
*/
final class ControllerMethodReader
{
/**
* @param string $namespace the default namespace
*/
public function __construct(private readonly string $namespace)
{
}
/**
* @param class-string $class
*
* @return list<array{route: string, handler: string}>
*/
public function read(string $class, string $defaultController = 'Home', string $defaultMethod = 'index'): array
{
$reflection = new ReflectionClass($class);
if ($reflection->isAbstract()) {
return [];
}
$classname = $reflection->getName();
$classShortname = $reflection->getShortName();
$output = [];
$uriByClass = $this->getUriByClass($classname);
if ($this->hasRemap($reflection)) {
$methodName = '_remap';
$routeWithoutController = $this->getRouteWithoutController(
$classShortname,
$defaultController,
$uriByClass,
$classname,
$methodName
);
$output = [...$output, ...$routeWithoutController];
$output[] = [
'route' => $uriByClass . '[/...]',
'handler' => '\\' . $classname . '::' . $methodName,
];
return $output;
}
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$methodName = $method->getName();
$route = $uriByClass . '/' . $methodName;
// Exclude BaseController and initController
// See system/Config/Routes.php
if (preg_match('#\AbaseController.*#', $route) === 1) {
continue;
}
if (preg_match('#.*/initController\z#', $route) === 1) {
continue;
}
if ($methodName === $defaultMethod) {
$routeWithoutController = $this->getRouteWithoutController(
$classShortname,
$defaultController,
$uriByClass,
$classname,
$methodName
);
$output = [...$output, ...$routeWithoutController];
$output[] = [
'route' => $uriByClass,
'handler' => '\\' . $classname . '::' . $methodName,
];
}
$output[] = [
'route' => $route . '[/...]',
'handler' => '\\' . $classname . '::' . $methodName,
];
}
return $output;
}
/**
* Whether the class has a _remap() method.
*/
private function hasRemap(ReflectionClass $class): bool
{
if ($class->hasMethod('_remap')) {
$remap = $class->getMethod('_remap');
return $remap->isPublic();
}
return false;
}
/**
* @param class-string $classname
*
* @return string URI path part from the folder(s) and controller
*/
private function getUriByClass(string $classname): string
{
// remove the namespace
$pattern = '/' . preg_quote($this->namespace, '/') . '/';
$class = ltrim(preg_replace($pattern, '', $classname), '\\');
$classParts = explode('\\', $class);
$classPath = '';
foreach ($classParts as $part) {
// make the first letter lowercase, because auto routing makes
// the URI path's first letter uppercase and search the controller
$classPath .= lcfirst($part) . '/';
}
return rtrim($classPath, '/');
}
/**
* Gets a route without default controller.
*/
private function getRouteWithoutController(
string $classShortname,
string $defaultController,
string $uriByClass,
string $classname,
string $methodName
): array {
if ($classShortname !== $defaultController) {
return [];
}
$pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#';
$routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/');
$routeWithoutController = $routeWithoutController ?: '/';
return [[
'route' => $routeWithoutController,
'handler' => '\\' . $classname . '::' . $methodName,
]];
}
}
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities\Routes;
use CodeIgniter\Config\Services;
use CodeIgniter\Filters\Filters;
use CodeIgniter\HTTP\Method;
use CodeIgniter\HTTP\Request;
use CodeIgniter\Router\Router;
use Config\Filters as FiltersConfig;
/**
* Collects filters for a route.
*
* @see \CodeIgniter\Commands\Utilities\Routes\FilterCollectorTest
*/
final class FilterCollector
{
public function __construct(
/**
* Whether to reset Defined Routes.
*
* If set to true, route filters are not found.
*/
private readonly bool $resetRoutes = false
) {
}
/**
* Returns filters for the URI
*
* @param string $method HTTP verb like `GET`,`POST` or `CLI`.
* @param string $uri URI path to find filters for
*
* @return array{before: list<string>, after: list<string>} array of filter alias or classname
*/
public function get(string $method, string $uri): array
{
if ($method === strtolower($method)) {
@trigger_error(
'Passing lowercase HTTP method "' . $method . '" is deprecated.'
. ' Use uppercase HTTP method like "' . strtoupper($method) . '".',
E_USER_DEPRECATED
);
}
/**
* @deprecated 4.5.0
* @TODO Remove this in the future.
*/
$method = strtoupper($method);
if ($method === 'CLI') {
return [
'before' => [],
'after' => [],
];
}
$request = Services::incomingrequest(null, false);
$request->setMethod($method);
$router = $this->createRouter($request);
$filters = $this->createFilters($request);
$finder = new FilterFinder($router, $filters);
return $finder->find($uri);
}
/**
* Returns Required Filters
*
* @return array{before: list<string>, after: list<string>} array of filter alias or classname
*/
public function getRequiredFilters(): array
{
$request = Services::incomingrequest(null, false);
$request->setMethod(Method::GET);
$router = $this->createRouter($request);
$filters = $this->createFilters($request);
$finder = new FilterFinder($router, $filters);
return $finder->getRequiredFilters();
}
private function createRouter(Request $request): Router
{
$routes = service('routes');
if ($this->resetRoutes) {
$routes->resetRoutes();
}
return new Router($routes, $request);
}
private function createFilters(Request $request): Filters
{
$config = config(FiltersConfig::class);
return new Filters($config, $request, service('response'));
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities\Routes;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\Filters\Filters;
use CodeIgniter\HTTP\Exceptions\BadRequestException;
use CodeIgniter\HTTP\Exceptions\RedirectException;
use CodeIgniter\Router\Router;
use Config\Feature;
/**
* Finds filters.
*
* @see \CodeIgniter\Commands\Utilities\Routes\FilterFinderTest
*/
final class FilterFinder
{
private readonly Router $router;
private readonly Filters $filters;
public function __construct(?Router $router = null, ?Filters $filters = null)
{
$this->router = $router ?? service('router');
$this->filters = $filters ?? service('filters');
}
private function getRouteFilters(string $uri): array
{
$this->router->handle($uri);
return $this->router->getFilters();
}
/**
* @param string $uri URI path to find filters for
*
* @return array{before: list<string>, after: list<string>} array of filter alias or classname
*/
public function find(string $uri): array
{
$this->filters->reset();
// Add route filters
try {
$routeFilters = $this->getRouteFilters($uri);
$this->filters->enableFilters($routeFilters, 'before');
$oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
if (! $oldFilterOrder) {
$routeFilters = array_reverse($routeFilters);
}
$this->filters->enableFilters($routeFilters, 'after');
$this->filters->initialize($uri);
return $this->filters->getFilters();
} catch (RedirectException) {
return [
'before' => [],
'after' => [],
];
} catch (BadRequestException|PageNotFoundException) {
return [
'before' => ['<unknown>'],
'after' => ['<unknown>'],
];
}
}
/**
* Returns Required Filters
*
* @return array{before: list<string>, after:list<string>}
*/
public function getRequiredFilters(): array
{
[$requiredBefore] = $this->filters->getRequiredFilters('before');
[$requiredAfter] = $this->filters->getRequiredFilters('after');
return [
'before' => $requiredBefore,
'after' => $requiredAfter,
];
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Commands\Utilities\Routes;
use CodeIgniter\Router\RouteCollection;
use Config\App;
/**
* Generate a sample URI path from route key regex.
*
* @see \CodeIgniter\Commands\Utilities\Routes\SampleURIGeneratorTest
*/
final class SampleURIGenerator
{
private readonly RouteCollection $routes;
/**
* Sample URI path for placeholder.
*
* @var array<string, string>
*/
private array $samples = [
'any' => '123/abc',
'segment' => 'abc_123',
'alphanum' => 'abc123',
'num' => '123',
'alpha' => 'abc',
'hash' => 'abc_123',
];
public function __construct(?RouteCollection $routes = null)
{
$this->routes = $routes ?? service('routes');
}
/**
* @param string $routeKey route key regex
*
* @return string sample URI path
*/
public function get(string $routeKey): string
{
$sampleUri = $routeKey;
if (str_contains($routeKey, '{locale}')) {
$sampleUri = str_replace(
'{locale}',
config(App::class)->defaultLocale,
$routeKey
);
}
foreach ($this->routes->getPlaceholders() as $placeholder => $regex) {
$sample = $this->samples[$placeholder] ?? '::unknown::';
$sampleUri = str_replace('(' . $regex . ')', $sample, $sampleUri);
}
// auto route
return str_replace('[/...]', '/1/2/3/4/5', $sampleUri);
}
}