Skip to content

Instantly share code, notes, and snippets.

@Zeronights
Last active January 16, 2023 14:19
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Zeronights/7b7d90fcf8d4daf9db0c to your computer and use it in GitHub Desktop.
Save Zeronights/7b7d90fcf8d4daf9db0c to your computer and use it in GitHub Desktop.
PHP: Getting use statements and aliases from class

ExtendedReflectionClass

This class was created from a need to get use statements and aliases from a class. The use statements are context sensitive. If there are multiple namespaces in the file, only the use statements from the namespace of the reflected class will be used.

The class file is read only up to the point where the class is instantiated to save memory and speed up the parsing process on large class files.

Usage

I will eventually move this class to its own repository and add it to composer. Until then, here are some basic usage examples.

MyCustomClass.php

<?php

namespace MyNamespace\Test;

use FooNamespace\FooClass;
use BarNamespace\BarClass as Bar;
use BazNamespace\BazClass as BazSpecial;

class MyCustomClass {

    protected $someDependencies = [];

    public function __construct(FooClass $foo, Bar $bar) {

        $someDependencies[] = $foo;
        $someDependencies[] = $bar;
    }
}

index.php

<?php

require 'ExtendedReflectionClass.php';
require 'MyCustomClass.php';

$class = new ExtendedReflectionClass('MyNamespace\Test\MyCustomClass');

$class->getUseStatements();    
// [
//     [
//         'class' => 'FooNamespace\FooClass',
//         'as' => 'FooClass'
//     ],
//     [
//         'class' => 'BarNamespace\BarClass',
//         'as' => 'Bar'
//     ],
//     [
//         'class' => 'BazNamespace\BazClass',
//         'as' => 'BazSpecial'
//     ]
// ]

$class->hasUseStatement('FooClass'); // true
$class->hasUseStatement('BarNamespace\BarClass'); // true
$class->hasUseStatement('BazSpecial'); // true

$class->hasUseStatement('SomeNamespace\SomeClass'); // false

Disclaimer

This was created as a proof of concept to answer my own StackOverflow question. As such, there are no tests or more comprehensive documentation. I will refine and release this class as a fully tested package soon. There is much which can be improved, especially in the tokenizeSource method. In the meantime, please have a look and tell me what you think.

<?php
/**
* The MIT License (MIT)
*
* Copyright (c) Ozgur (Ozzy) Giritli <ozgur@zeronights.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
class ExtendedReflectionClass extends ReflectionClass {
/**
* Array of use statements for class.
*
* @var array
*/
protected $useStatements = [];
/**
* Check if use statements have been parsed.
*
* @var boolean
*/
protected $useStatementsParsed = false;
/**
* Parse class file and get use statements from current namespace.
*
* @return void
*/
protected function parseUseStatements() {
if ($this->useStatementsParsed) {
return $this->useStatements;
}
if (!$this->isUserDefined()) {
throw new RuntimeException('Must parse use statements from user defined classes.');
}
$source = $this->readFileSource();
$this->useStatements = $this->tokenizeSource($source);
$this->useStatementsParsed = true;
return $this->useStatements;
}
/**
* Read file source up to the line where our class is defined.
*
* @return string
*/
private function readFileSource() {
$file = fopen($this->getFileName(), 'r');
$line = 0;
$source = '';
while (!feof($file)) {
++$line;
if ($line >= $this->getStartLine()) {
break;
}
$source .= fgets($file);
}
fclose($file);
return $source;
}
/**
* Parse the use statements from read source by
* tokenizing and reading the tokens. Returns
* an array of use statements and aliases.
*
* @param string $source
* @return array
*/
private function tokenizeSource($source) {
$tokens = token_get_all($source);
$builtNamespace = '';
$buildingNamespace = false;
$matchedNamespace = false;
$useStatements = [];
$record = false;
$currentUse = [
'class' => '',
'as' => ''
];
foreach ($tokens as $token) {
if ($token[0] === T_NAMESPACE) {
$buildingNamespace = true;
if ($matchedNamespace) {
break;
}
}
if ($buildingNamespace) {
if ($token === ';') {
$buildingNamespace = false;
continue;
}
switch ($token[0]) {
case T_STRING:
case T_NS_SEPARATOR:
$builtNamespace .= $token[1];
break;
}
continue;
}
if ($token === ';' || !is_array($token)) {
if ($record) {
$useStatements[] = $currentUse;
$record = false;
$currentUse = [
'class' => '',
'as' => ''
];
}
continue;
}
if ($token[0] === T_CLASS) {
break;
}
if (strcasecmp($builtNamespace, $this->getNamespaceName()) === 0) {
$matchedNamespace = true;
}
if ($matchedNamespace) {
if ($token[0] === T_USE) {
$record = 'class';
}
if ($token[0] === T_AS) {
$record = 'as';
}
if ($record) {
switch ($token[0]) {
case T_STRING:
case T_NS_SEPARATOR:
if ($record) {
$currentUse[$record] .= $token[1];
}
break;
}
}
}
if ($token[2] >= $this->getStartLine()) {
break;
}
}
// Make sure the as key has the name of the class even
// if there is no alias in the use statement.
foreach ($useStatements as &$useStatement) {
if (empty($useStatement['as'])) {
$useStatement['as'] = basename($useStatement['class']);
}
}
return $useStatements;
}
/**
* Return array of use statements from class.
*
* @return array
*/
public function getUseStatements() {
return $this->parseUseStatements();
}
/**
* Check if class is using a class or an alias of a class.
*
* @param string $class
* @return boolean
*/
public function hasUseStatement($class) {
$useStatements = $this->parseUseStatements();
return
array_search($class, array_column($useStatements, 'class')) ||
array_search($class, array_column($useStatements, 'as'));
}
}
@jirigracik
Copy link

This is a windows specific implementation, it does not work correctly anywhere else. Look at the first parameter of basename function.
http://php.net/manual/en/function.basename.php

@bjoern-tantau
Copy link

Instead of basename you may use $useStatement['as'] = last(explode('\\', $useStatement['class']));.

I also had to override getParentClass to get the parent class with the extended reflection.

    public function getParentClass()
    {
        if ($parent = parent::getParentClass()) {
            return new static($parent->getName());
        }
        return false;
    }

@carpabg
Copy link

carpabg commented Sep 17, 2019

I have this class, but when I use getUseStatements() I'm receiving this array where the as key seems to be wrong. And hasUseStatement('UserPermissions') or hasUseStatement('AdminEmbedModels\UserPermissions') always return false

<?php

namespace AdminModels;

use AdminEmbedModels\UserPermissions;

class User extends \AdminModel{
array(1) {
  [0]=>
  array(2) {
    ["class"]=>
    string(32) "AdminEmbedModels\UserPermissions"
    ["as"]=>
    string(32) "AdminEmbedModels\UserPermissions"
  }
}

I don't know what's happening, could you help me? Thank you.

@8-3
Copy link

8-3 commented Apr 27, 2020

Thank you, this has been a great help. At those that use this implementation as well: please not that it does currently not support the following two use statement declaration styles:

  • use \some\namespace as MyNamespace;
  • use \some\namespace\{SomeClass as MyClass};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment