bnomei/autoloader-for-kirby

View on GitHub
classes/Autoloader.php

Summary

Maintainability
F
3 days
Test Coverage
A
95%
File `Autoloader.php` has 395 lines of code (exceeds 250 allowed). Consider refactoring.
<?php
 
declare(strict_types=1);
 
namespace Bnomei;
 
use Closure;
use Exception;
use Kirby\Toolkit\A;
use Spyc;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Yaml\Yaml;
 
`Autoloader` has 24 functions (exceeds 20 allowed). Consider refactoring.
final class Autoloader
{
// exclude files like filename.config.(php|yml)
public const PHP = '/^[\w\d\-\_]+\.php$/';
 
public const ANY_PHP = '/^[\w\d\-\_\.]+\.php$/';
 
public const BLOCK_PHP = '/^[\w\d\-\_]+(Block)\.php$/';
 
public const PAGE_PHP = '/^[\w\d\-\_]+(Page)\.php$/';
 
public const USER_PHP = '/^[\w\d\-\_]+(User)\.php$/';
 
public const YML = '/^[\w\d\-\_]+\.yml$/';
 
public const ANY_YML = '/^[\w\d\-\_\.]+\.yml$/';
 
public const PHP_OR_HTMLPHP = '/^[\w\d\-\_]+(\.html)?\.php$/';
 
public const PHP_OR_YML = '/^[\w\d\-\_]+\.(php|yml)$/';
 
public const ANY_PHP_OR_YML = '/^[\w\d\-\_\.]+\.(php|yml)$/';
 
public const PHP_OR_YML_OR_JSON = '/^[\w\d\-\_]+\.(php|yml|json)$/';
 
public const ANY_PHP_OR_YML_OR_JSON = '/^[\w\d\-\_\.]+\.(php|yml|json)$/';
 
private static ?self $singleton = null;
 
/** @var array */
private $options;
 
/** @var array */
private $registry;
 
Method `__construct` has 102 lines of code (exceeds 25 allowed). Consider refactoring.
public function __construct(array $options = [])
{
$this->options = self::array_merge_recursive_distinct([
// we can not read the kirby options since we are loading
// while kirby is booting, but once spyc is removed we can
// default to symfony yaml
// TODO: maybe with a Kirby::version() check
'yaml.handler' => 'spyc', // spyc or symfony
'blueprints' => [
'folder' => 'blueprints',
'name' => self::ANY_PHP_OR_YML,
'key' => 'relativepath',
'require' => true, // false
'transform' => fn ($key) => strtolower($key),
],
'classes' => [
'folder' => 'classes',
'name' => self::PHP,
'key' => 'classname',
'require' => false,
'transform' => fn ($key) => strtolower($key),
'map' => [],
],
'collections' => [
'folder' => 'collections',
'name' => self::ANY_PHP,
'key' => 'relativepath',
'require' => true,
'transform' => false,
],
'commands' => [
'folder' => 'commands',
'name' => self::ANY_PHP,
'key' => 'relativepath',
'require' => true,
'transform' => fn ($key) => str_replace('/', ':', $key),
],
'controllers' => [
'folder' => 'controllers',
'name' => self::ANY_PHP,
'key' => 'filename',
'require' => true,
'transform' => fn ($key) => self::pascalToKebabCase($key),
],
'blockModels' => [
'folder' => 'models',
'name' => self::BLOCK_PHP,
'key' => 'classname',
'require' => false,
'transform' => fn ($key) => self::pascalToKebabCase($key),
'map' => [],
],
'pageModels' => [
'folder' => 'models',
'name' => self::PAGE_PHP,
'key' => 'classname',
'require' => false,
'transform' => fn ($key) => self::pascalToKebabCase($key),
'map' => [],
],
'routes' => [
'folder' => 'routes',
'name' => self::ANY_PHP,
'key' => 'route',
'require' => true,
'transform' => false,
],
'apiroutes' => [
'folder' => 'api/routes',
'name' => self::ANY_PHP,
'key' => 'route',
'require' => true,
'transform' => false,
],
'userModels' => [
'folder' => 'models',
'name' => self::USER_PHP,
'key' => 'classname',
'require' => false,
'transform' => fn ($key) => self::pascalToKebabCase($key),
'map' => [],
],
'snippets' => [
'folder' => 'snippets',
'name' => self::ANY_PHP,
'key' => 'relativepath',
'require' => false,
'transform' => false,
],
'templates' => [
'folder' => 'templates',
'name' => self::ANY_PHP,
'key' => 'filename',
'require' => false,
'transform' => fn ($key) => strtolower($key),
],
'translations' => [
'folder' => 'translations',
'name' => self::ANY_PHP_OR_YML_OR_JSON,
'key' => 'filename',
'require' => true,
'transform' => fn ($key) => strtolower($key),
],
], $options);
 
if (! array_key_exists('dir', $this->options)) {
throw new Exception('Autoloader needs a directory to start scanning at.');
}
 
$this->registry = [];
}
 
public function dir(): string
{
return $this->options['dir'];
}
 
Function `registry` has a Cognitive Complexity of 72 (exceeds 5 allowed). Consider refactoring.
Method `registry` has 128 lines of code (exceeds 25 allowed). Consider refactoring.
private function registry(string $type): array
{
// only register once
if (array_key_exists($type, $this->registry)) {
return $this->registry[$type];
}
 
$options = $this->options[$type];
$dir = $this->options['dir'].'/'.$options['folder'];
if (! file_exists($dir) || ! is_dir($dir)) {
return [];
}
 
$this->registry[$type] = [];
$finder = (new Finder)->files()
->name($options['name'])
->in($dir);
 
foreach ($finder as $file) {
$key = '';
$class = '';
$split = explode('.', $file->getPathname());
$extension = array_pop($split);
if ($options['key'] === 'relativepath' || $options['key'] === 'route') {
$key = $file->getRelativePathname();
$key = str_replace('.'.$extension, '', $key);
$key = str_replace('\\', '/', $key); // windows
} elseif ($options['key'] === 'filename') {
$key = basename($file->getRelativePathname());
$key = str_replace('.'.$extension, '', $key);
} elseif ($options['key'] === 'classname') {
$key = $file->getRelativePathname();
$key = str_replace('.'.$extension, '', $key);
$class = str_replace('/', '\\', $key);
if ($classFile = file_get_contents($file->getPathname())) {
if (preg_match('/^namespace (.*);$/im', $classFile, $matches) === 1) {
$class = str_replace($matches[1].'\\', '', $class);
$class = $matches[1].'\\'.$class;
}
}
$this->registry[$type]['map'][$class] = $file->getRelativePathname();
 
foreach (['Page', 'User', 'Block'] as $suffix) {
$at = strpos($key, $suffix);
if ($at === strlen($key) - strlen($suffix)) {
$key = substr($key, 0, -strlen($suffix));
}
}
}
 
if (empty($key)) {
continue;
} else {
if ($options['transform'] instanceof Closure) { // apply transform function
$key = $options['transform']($key);
}
 
$key = strval($key); // in case key looks like a number but should be a string
}
if (! empty($class)) {
$this->registry[$type][$key] = $class;
}
 
Consider simplifying this complex logical expression.
if ($options['key'] === 'classname') {
$this->registry[$type][$key] = $class;
} elseif ($options['key'] === 'route') {
// Author: @tobimori
$pattern = strtolower($file->getRelativePathname());
$pattern = (string) preg_replace('~(.*)'.preg_quote('.php', '~').'~', '$1'.'', $pattern, 1); // replace extension at end
$pattern = (string) preg_replace('~(.*)'.preg_quote('index', '~').'~', '$1'.'', $pattern, 1); // replace index at end, for root of folder, but not in paths etc.
 
$route = require $file->getRealPath();
 
// check if return is actually an array (if additional stuff is specified, e.g. method or language) or returns a function
if (is_array($route) || $route instanceof Closure) {
$this->registry[$type][] = array_merge(
[
'pattern' => /*'/' . */ $pattern,
'action' => $route instanceof Closure ? $route : null,
],
is_array($route) ? $route : []
);
}
} elseif ($options['folder'] === 'blueprints' && $extension && strtolower($extension) === 'php') {
$path = $file->getPathname();
$this->registry[$type][$key] = include $path; // require will link, include will read
if (is_array($this->registry[$type][$key])) {
$kk = explode('/', $key);
$this->registry[$type][$key]['name'] = array_pop($kk);
}
} elseif ($options['require'] && $extension && strtolower($extension) === 'php') {
$path = $file->getPathname();
$this->registry[$type][$key] = include $path; // require will link, include will read
} elseif ($options['require'] && $extension && strtolower($extension) === 'json') {
$path = $file->getPathname();
$this->registry[$type][$key] = json_decode((string) file_get_contents($path), true);
} elseif ($options['require'] && $extension && strtolower($extension) === 'yml') {
$path = $file->getPathname();
// remove BOM
$yaml = str_replace("\xEF\xBB\xBF", '', (string) file_get_contents($path));
if ($this->options['yaml.handler'] === 'symfony') {
$this->registry[$type][$key] = Yaml::parse($yaml);
} else {
$this->registry[$type][$key] = Spyc::YAMLLoadString($yaml);
}
if (is_array($this->registry[$type][$key]) && $options['folder'] === 'blueprints') {
$kk = explode('/', $key);
$this->registry[$type][$key]['name'] = array_pop($kk);
}
} else {
$this->registry[$type][$key] = $file->getRealPath();
}
}
 
if ($options['key'] === 'classname' && array_key_exists('map', $this->registry[$type])) {
// sort by \ in FQCN count desc
// within same count sort alpha
$map = array_flip($this->registry[$type]['map']);
uasort($map, function ($a, $b) {
$ca = substr_count($a, '\\');
$cb = substr_count($b, '\\');
if ($ca === $cb) {
$alpha = [$a, $b];
sort($alpha);
 
return $alpha[0] === $a ? -1 : 1;
}
 
return $ca < $cb ? 1 : -1;
});
$map = array_flip($map);
$this->load($map, $this->options['dir'].'/'.$options['folder']);
 
// load blueprints from classes
foreach ($map as $class => $file) {
if (! is_string($class)) {
continue;
}
// if instance of class has static method registerBlueprintExtension
if (class_exists($class) && method_exists($class, 'blueprintFromAttributes')) {
// register blueprints now, using and empty array would prevent the loading later
if (! array_key_exists('blueprints', $this->registry)) {
$this->registry['blueprints'] = $this->blueprints();
}
// call blueprintFromAttributes
$blueprint = $class::blueprintFromAttributes();
// if blueprint is not empty
if (! empty($blueprint)) {
// merge with existing blueprint
$this->registry['blueprints'] = array_merge($this->registry['blueprints'], $blueprint);
}
}
}
 
unset($this->registry[$type]['map']);
}
 
Avoid too many `return` statements within this method.
return $this->registry[$type];
}
 
public function blueprints(): array
{
return $this->registry('blueprints');
}
 
public function classes(?string $folder = null): array
{
if ($folder) {
$this->options['classes']['folder'] = $folder;
}
 
return $this->registry('classes');
}
 
public function collections(): array
{
return $this->registry('collections');
}
 
public function commands(): array
{
return $this->registry('commands');
}
 
public function controllers(): array
{
return $this->registry('controllers');
}
 
public function blockModels(): array
{
return $this->registry('blockModels');
}
 
public function pageModels(): array
{
return $this->registry('pageModels');
}
 
public function routes(): array
{
return $this->registry('routes');
}
 
public function apiRoutes(): array
{
return $this->registry('apiroutes');
}
 
public function userModels(): array
{
return $this->registry('userModels');
}
 
public function snippets(): array
{
return $this->registry('snippets');
}
 
public function templates(): array
{
return $this->registry('templates');
}
 
public function translations(): array
{
return $this->registry('translations');
}
 
public function toArray(array $merge = []): array
{
$this->classes();
 
// merge each on its own to allow cross loading between registries
// like a pageModel to load a blueprint
$types = [
fn () => ['blueprints' => $this->blueprints()],
fn () => ['collections' => $this->collections()],
fn () => ['commands' => $this->commands()],
fn () => ['controllers' => $this->controllers()],
fn () => ['blockModels' => $this->blockModels()],
fn () => ['pageModels' => $this->pageModels()],
fn () => ['userModels' => $this->userModels()],
fn () => ['snippets' => $this->snippets()],
fn () => ['templates' => $this->templates()],
fn () => ['translations' => $this->translations()],
fn () => ['api' => ['routes' => $this->apiRoutes()]],
fn () => ['routes' => $this->routes()],
];
foreach ($types as $callback) {
$c = (array) $callback();
$r = (array) $this->registry;
$this->registry = array_merge($r, $c);
}
 
// merge on top but do not store in the registry
return array_merge_recursive($this->registry, $merge);
}
 
public static function pascalToKebabCase(string $string): string
{
return ltrim(strtolower((string) preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '-$0', $string)), '-');
}
 
public static function pascalToCamelCase(string $string): string
{
return lcfirst($string);
}
 
public static function pascalToDotCase(string $string): string
{
return ltrim(strtolower((string) preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '.$0', $string)), '.');
}
 
public static function array_merge_recursive_distinct(array $array1, array $array2): array
{
$merged = $array1;
 
foreach ($array2 as $key => $value) {
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
$merged[$key] = self::array_merge_recursive_distinct($merged[$key], $value);
} else {
$merged[$key] = $value;
}
}
 
return $merged;
}
 
public static function singletonClear(): void
{
self::$singleton = null;
}
 
public static function singleton(array $options = []): self
{
if (self::$singleton && self::$singleton->dir() === $options['dir']) {
return self::$singleton;
}
self::$singleton = new self($options);
 
return self::$singleton;
}
 
// https://github.com/getkirby/kirby/blob/c77ccb82944b5fa0e3a453b4e203bd697e96330d/config/helpers.php#L505
/**
* A super simple class autoloader
*
* @return void
*/
private function load(array $classmap, ?string $base = null)
{
// convert all classnames to lowercase
$classmap = array_change_key_case($classmap);
 
spl_autoload_register(function ($class) use ($classmap, $base) {
$class = strtolower($class);
 
if (! isset($classmap[$class])) {
return;
}
 
if ($base) {
include $base.'/'.$classmap[$class];
} else {
include $classmap[$class];
}
});
}
}