Skip to content

Instantly share code, notes, and snippets.

@WinterSilence
Last active July 4, 2022 00:11
Show Gist options
  • Save WinterSilence/1d5fe67ec296b7c099264d31ba435725 to your computer and use it in GitHub Desktop.
Save WinterSilence/1d5fe67ec296b7c099264d31ba435725 to your computer and use it in GitHub Desktop.
Yii2 application reflection
<?php
namespace yii\helpers;
use ReflectionClass;
use ReflectionMethod;
use Yii;
use yii\base\Action;
use yii\base\Application;
use yii\base\Behavior;
use yii\base\Controller;
use yii\helpers\FileHelper;
use yii\helpers\Inflector;
use function array_merge;
use function class_exists;
use function is_string;
use function strlen;
use function str_replace;
use function substr;
use function preg_match;
use function natsort;
/**
* Yii application's reflection.
*/
class AppHelper
{
/**
* @var Application The application instance to reflect
*/
protected $app;
/**
* @var string[] The cached list of the application controllers as class(FCQN)/ID pairs
*/
protected $controllers;
/**
* Create reflection.
*
* @param Application|null $application The application instance or NULL to reflect current application
*/
public function __construct(Application $application = null)
{
$this->app = $application ?: Yii::$app;
}
/**
* Returns application controllers as class(FCQN)/identifier pairs.
*
* @param bool $refresh If TRUE, refresh list, else, return cached list
* @return string[]
*/
public function getControllers(bool $refresh = false): array
{
if ($refresh || !isset($this->controllers)) {
$files = FileHelper::findFiles(
$this->app->controllerPath,
['only' => ['*Controller.php']]
);
$prefixLength = strlen($this->app->controllerPath) + 1;
$ns = rtrim($this->app->controllerNamespace, '\\') . '\\';
$this->controllers = [];
foreach ($files as $file) {
// for example, converts file name '@app/controllers/foo/BarBazController.php' to ID 'foo/bar-baz'
$file = substr($file, $prefixLength, -14);
$class = $ns . str_replace(DIRECTORY_SEPARATOR, '\\', $file) . 'Controller';
$this->controllers[$class] = $this->controllerClassToId($class);
}
}
return $this->controllers;
}
/**
* Returns inline actions of given controller.
*
* @param Controller $controller The controller instance
* @return string[] An array of inline actions as method/identifier pairs
*/
public function getControllerInlineActions(Controller $controller): array
{
$actions = [];
$class = new ReflectionClass($controller);
foreach ($class->getMethods() as $method) {
if ($this->isActionMethod($method)) {
$actions[$method->name] = $this->actionMethodToId($method->name);
}
}
return $actions;
}
/**
* Returns standalone actions of given controller.
*
* @param Controller $controller The controller instance
* @return string[] An array of standalone actions as class/identifier pairs
*/
public function getControllerStandaloneActions(Controller $controller): array
{
$actions = [];
foreach ($controller->actions() as $id => $action) {
if ($action instanceof Action) {
$class = get_class($action);
$id = $action->id;
} elseif (is_string($action)) {
$class = $action;
} else {
/** @var array $action */
$class = $action['class'];
}
$actions[$class] = $id;
}
return $actions;
}
/**
* Returns all actions of the given controller.
*
* @param Controller $controller The controller instance
* @param bool $sort If true, sort actions by IDs
* @return array An array of action ID/class pairs
*/
public function getControllerActions(Controller $controller, bool $sort = false): array
{
$actions = array_merge(
$this->getControllerInlineActions($controller),
$this->getControllerStandaloneActions($controller)
);
if ($sort) {
natsort($actions);
}
return $actions;
}
/**
* Validates if the given class is a valid web/console controller class.
*
* @param string $className The class name to test
* @param bool $ignoreApp If TRUE, not checking inheritance from application controller
* @return bool
* @throws \ReflectionException
*/
public function isControllerClass(string $className, bool $ignoreApp = false): bool
{
if (!class_exists($className)) {
return false;
}
$class = new ReflectionClass($className);
if ($class->isAbstract()) {
return false;
}
if ($ignoreApp) {
$parentClass = Controller::class;
} else {
$parentClass = $this->app instanceof \yii\console\Application
? \yii\console\Controller::class
: \yii\web\Controller::class;
}
return $class->isSubclassOf($parentClass);
}
/**
* Check is this application is runned in console.
*
* @return bool
*/
public function isConsole(): bool
{
if ($this->app instanceof \yii\console\Application) {
return true;
}
return $this->app->request instanceof \yii\base\Request
&& $this->app->request->isConsoleRequest;
}
/**
* Checks action method by name.
*
* @param ReflectionMethod $method The method to test
* @return bool
*/
public function isActionMethod(ReflectionMethod $method): bool
{
return $method->isPublic()
&& !$method->isStatic()
&& preg_match('/^action[A-Z][A-Za-z\d]+$/', $method->name) === 1;
}
/**
* Converts the class name (FCQN) to controller identifier.
*
* @param string $name The class name (for example, `app\controllers\SiteController`)
* @return string The controller ID
*/
public function controllerClassToId(string $name): string
{
$ns = rtrim($this->app->controllerNamespace, '\\');
// trim namespace and `Controller` suffix
$name = substr(ltrim($name, '\\'), strlen($ns) + 1, -10);
return str_replace(['\\', '/-'], '/', Inflector::camel2id($name, '-', true));
}
/**
* Converts controller's method name to action identifier.
*
* @param string $name The method name (for example, `actionFooBar`)
* @return string The action ID
*/
public function actionMethodToId(string $name): string
{
return Inflector::camel2id(substr($name, 6));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment