brainworxx/kreXX

View on GitHub
src/Service/Reflection/ReflectionClass.php

Summary

Maintainability
A
0 mins
Test Coverage
B
86%
<?php

/**
 * kreXX: Krumo eXXtended
 *
 * kreXX is a debugging tool, which displays structured information
 * about any PHP object. It is a nice replacement for print_r() or var_dump()
 * which are used by a lot of PHP developers.
 *
 * kreXX is a fork of Krumo, which was originally written by:
 * Kaloyan K. Tsvetkov <kaloyan@kaloyan.info>
 *
 * @author
 *   brainworXX GmbH <info@brainworxx.de>
 *
 * @license
 *   http://opensource.org/licenses/LGPL-2.1
 *
 *   GNU Lesser General Public License Version 2.1
 *
 *   kreXX Copyright (C) 2014-2024 Brainworxx GmbH
 *
 *   This library is free software; you can redistribute it and/or modify it
 *   under the terms of the GNU Lesser General Public License as published by
 *   the Free Software Foundation; either version 2.1 of the License, or (at
 *   your option) any later version.
 *   This library is distributed in the hope that it will be useful, but WITHOUT
 *   ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 *   FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 *   for more details.
 *   You should have received a copy of the GNU Lesser General Public License
 *   along with this library; if not, write to the Free Software Foundation,
 *   Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

declare(strict_types=1);

namespace Brainworxx\Krexx\Service\Reflection;

use ReflectionException;
use ReflectionProperty;
use Throwable;
use SplObjectStorage;
use Krexx;

/**
 * Added a better possibility to retrieve the object values.
 */
class ReflectionClass extends \ReflectionClass
{
    /**
     * The object, cast into an array.
     *
     * @var array
     */
    protected array $objectArray = [];

    /**
     * The object we are currently analysing.
     *
     * @var object|string
     */
    protected $data;

    /**
     * Storage for unset properties.
     *
     * @var \SplObjectStorage
     */
    protected SplObjectStorage $unsetPropertyStorage;

    /**
     * ReflectionClass constructor.
     *
     * @param object|string $data
     *   The class we are currently analysing.
     *
     * @throws \ReflectionException
     */
    public function __construct($data)
    {
        // Retrieve the class variables.
        $this->objectArray = (array) $data;
        // Remember the current object.
        $this->data = $data;
        // Init our unset object storage;
        $this->unsetPropertyStorage = new SplObjectStorage();

        parent::__construct($data);
    }

    /**
     * Retrieve the value from the object, if possible.
     *
     * @param \ReflectionProperty $refProperty
     *   The reflection of the property we are analysing.
     *
     * @return mixed;
     *   The retrieved value.
     */
    public function retrieveValue(ReflectionProperty $refProperty)
    {
        $propName = $refProperty->getName();
        $lookup = [
            // Protected properties
            "\0*\0" . $propName,
            // Inherited properties
            "\0" . $refProperty->getDeclaringClass()->getName() . "\0" . $propName,
            // Public properties.
            $propName
        ];

        foreach ($lookup as $arrayKey) {
            if (array_key_exists($arrayKey, $this->objectArray)) {
                return $this->objectArray[$arrayKey];
            }
        }

        try {
            if ($refProperty->isStatic()) {
                // Static values are not inside the value array.
                $refProperty->setAccessible(true);
                return $refProperty->getValue($this->data);
            }
        } catch (Throwable $throwable) {
            // Do nothing.
            // We ignore this one.
        }

        return $this->retrieveEsotericValue($refProperty);
    }

    /**
     * Retriever the value by more esoteric means.
     *
     * And by this I mean taking care of two PHP bugs:
     *   - Properties with integer names
     *   - Hidden public properties of the ext-dom objects
     *   - Hidden protected properties of the \DateTime object
     *
     * @param \ReflectionProperty $refProperty
     *   The reflection of the property that we are accessing.
     *
     * @return mixed
     */
    protected function retrieveEsotericValue(ReflectionProperty $refProperty)
    {
        $propName = $refProperty->getName();
        if ($refProperty instanceof UndeclaredProperty && is_numeric($propName)) {
            // We are facing a numeric property name (yes, that is possible).
            // To be honest, this one of the most bizarre things I've encountered so
            // far. Depending on your PHP version, that value may not be accessible
            // via normal means from the array we have got here. And no, we are not
            // accessing the object directly.
            return array_values($this->objectArray)[
                array_search($propName, array_keys($this->objectArray))
            ];
        }

        if ($refProperty instanceof HiddenProperty) {
            // We need to access the value directly.
            // But first we must make sure that the hosting cms does not do
            // something stupid. Accessing this value directly it probably
            // a bad idea, but the only way to get the value.
            set_error_handler(Krexx::$pool->retrieveErrorCallback());
            try {
                $result = $this->data->$propName;
                restore_error_handler();
                return $result;
            } catch (Throwable $exception) {
                // Do nothing.
                // Looks like somebody did not like me accessing it directly.
            }
            restore_error_handler();
        }

        $this->unsetPropertyStorage->attach($refProperty);
        return null;
    }

    /**
     * Was this propery unset?
     *
     * The info is only available if you retrieve the value beforehand.
     *
     * @param \ReflectionProperty $reflectionProperty
     * @return bool
     */
    public function isPropertyUnset(ReflectionProperty $reflectionProperty): bool
    {
        return $this->unsetPropertyStorage->contains($reflectionProperty);
    }

    /**
     * Get the instance, from which this reflection was created.
     *
     * @return object
     */
    public function getData(): object
    {
        return $this->data;
    }

    /**
     * Retrieve the actually implemented interfaces.
     *
     * @return ReflectionClass[]
     *   Array with the interfaces.
     */
    public function getInterfaces(): array
    {
        // Compare the names with the ones from the parent.
        $parent = $this->getParentClass();
        $interfaceNames = $this->getInterfaceNames();
        if ($parent !== false) {
            $interfaceNames = array_diff($interfaceNames, $parent->getInterfaceNames());
        }
        if (empty($interfaceNames)) {
            return [];
        }

        // Get the instances.
        $result = [];
        foreach ($interfaceNames as $interfaceName) {
            try {
                $result[$interfaceName] = new ReflectionClass($interfaceName);
            } catch (ReflectionException $exception) {
                // Do nothing. We skip this one.
                // Not sure how this could happen.
            }
        }

        return $result;
    }

    /**
     * Wrapper around the getTraits, to make sure we get our ReflectionClass.
     *
     * @return array|\ReflectionClass[]
     */
    public function getTraits(): array
    {
        $traits = parent::getTraitNames();
        if (empty($traits)) {
            return [];
        }

        $result = [];
        foreach ($traits as $trait) {
            try {
                $result[$trait] = new ReflectionClass($trait);
            } catch (ReflectionException $exception) {
                // We skip this one.
            }
        }

        return $result;
    }

    /**
     * Wrapper around the getParentClass, to make sure we get our ReflectionClass.
     *
     * @return bool|\ReflectionClass
     */
    #[\ReturnTypeWillChange]
    public function getParentClass()
    {
        $result = false;
        $parent = parent::getParentClass();
        if (!empty($parent)) {
            try {
                $result = new ReflectionClass($parent->name);
            } catch (ReflectionException $e) {
                // Do nothing.
            }
        }

        return $result;
    }
}