DEV Community

Anders Björkland
Anders Björkland

Posted on

How I stumbled onto the Reflection API

Coding for fun can sometime lead you into unexpected neighbourhoods. This is what happened to me this week. I've written previously about the micro-framework Framework-X and this week I was building another project with it and was in need of hooking it up with a database. Framework-X supports async and non-blocking execution. For this reason, while it is possible, the project recommends going for non-blocking database access. So it is absolutely possible to use Doctrine but it is not the reactive way that Framework-X promotes. So this had me leave the known grounds of Doctrine to explore a more "hands-on" approach of communicating with the database.

So I though, anyway.

Being me, I like to explore new subjects and see how far I can go with my current proficiency. I come from a background where all I've ever done has almost exclusively been "object oriented", but now being left without Doctrine (a popular PHP ORM) I saw an opportunity to build similar (but way more simplistic) functionality on my own.

I didn't think I'd set out to build a mini-ORM in my project but that's where I've found myself. I have entity-classes, or what you could describe as "plain old PHP-objects" (POPOs). These POPOs describe their own properties with types. Theses types may describe a relation to another entity as well as a one-to-many relation, reflected as an array in the other class (many-to-one). I don't have a many-to-many relation in the project currently. So a POPO could look like this:

<?php 

namespace App\Entity;

class Category
{
    /**
     * @var string $uuid - UUID version 4 variant 1
     */
    private string $uuid = '';

    private string $name = '';

    /**
     * @var Category[] $childCategories
     */
    private array $childCategories = [];

    private ?Category $parentCategory = null;

    public function setUuid(string $uuid = ''): self
    {
        $this->uuid = $uuid;
        return $this;
    }

    public function getUuid(): string
    {
        return $this->uuid;
    }

    public function setName(string $name): self
    {
        $this->name = $name;
        return $this;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setChildCategories(array $childCategories): self
    {
        $this->childCategories = $childCategories;
        return $this;
    }

    public function getChildCategories(): array
    {
        return $this->childCategories;
    }

    public function addChildCategory(Category $childCategory): self
    {
        if (!$this->hasChildCategory($childCategory)){
            $this->childCategories[] = $childCategory;
        }
        return $this;
    }

    public function removeChildCategory(Category $childCategory): self
    {
        $this->childCategories = array_filter($this->childCategories, function ($category) use ($childCategory) {
            return $category->getUuid() !== $childCategory->getUuid();
        });

        return $this;
    }

    public function hasChildCategory(Category $childCategory): bool
    {
        return in_array($childCategory, $this->childCategories);
    }

    public function setParentCategory(Category $parentCategory): self
    {
        $parentCategory->addChildCategory($this);
        $this->parentCategory = $parentCategory;
        return $this;
    }

    public function getParentCategory(): ?Category
    {
        return $this->parentCategory;
    }
}
Enter fullscreen mode Exit fullscreen mode

Having POPOs like this, I wanted to be able to store these objects in the database, as well as fetch results from the database and turn them back into objects. For this to work for me, I needed to know what types each property is. In the case of arrays, I needed to know the type of the objects inside the array. Normal properties simply has a stated type, for arrays I use the @var annotation:

    /**
     * @var Category[] $childCategories
     */
    private array $childCategories = [];
Enter fullscreen mode Exit fullscreen mode

For each property I also have a getter and a setter. Knowing all this, I can get all relevant properties when I want to store them in the database. With enough insight into the class I can turn each property into a corresponding SQL-type: a string into VARCHAR for MySQL for example, and a reference into a VARCHAR (as I'm using UUID) along with a foreign-key.

But from where do I get the relevant insight?!

That would be the ReflectionClass! Without it I would have to parse each class-file line-by-line, but instead I can use getMethods, getProperties, getType, getDocComment. What I found most fun in this project was fetching the annotation type hint for the arrays. I built a method in a utility class to fetch those:


    // Inside a class called App\Utility\EntityReflection.php

    public static function getPropertyTypeFromComment(string $entityClass, string $property): ?string
    {
        $reflectionClass = new \ReflectionClass($entityClass);
        $reflectionProperty = $reflectionClass->getProperty($property);
        $propertyComment = $reflectionProperty->getDocComment();

        $refType = preg_grep('/([A-Z])\w+/', explode(" ", $propertyComment));

        if (count($refType) > 0) {
            $refType = array_pop($refType);
        } else {
            return null;
        }

        if ($refType === null) {
            return null;
        }

        if (strpos($refType, '[]') !== false) {
            return $refType;
        }

        return null;    
    }
Enter fullscreen mode Exit fullscreen mode

I could call this with $commentType = EntityReflection::getPropertyTypeFromComment(Category::class, 'categories');, which would return Category[].

And that's how I stumbled upon the Reflection API. I now use it successfully storing and fetching objects from a relational database. But that's another story. So have you used reflection in any of your objects, and how did you like it? As I may have hinted at, I find it pretty fun, magical and very useful!

Discussion (6)

Collapse
dawiddahl profile image
Dawid Dahl

Oh cool!

I myself stumbled over the reflection API when I was researching how to implement dependency injection containers. But at the time I couldn’t figure out other awesome use cases for it. Until now! Grabbing hold of docstring information at runtime is an awesome application of reflection, thanks a lot for teaching me! 🙂🙌🏻

Collapse
andersbjorkland profile image
Anders Björkland Author

Cool! That sounds like a neat use-case for it too 😀

Collapse
cess11 profile image
PNS11

Looks like there's a bug in Category::addChildCategory(), it'll only add objects that are already in the array.

Don't see a constructor so how do you instantiate Category? Got some generic wrapper class that consumes data and target class name and then figures out setter-names?

Collapse
andersbjorkland profile image
Anders Björkland Author

Good catch. It's supposed to be a ! in there.

As for constructor, it is inferred. I configure the properties with some default values (just empty string or empty array). Then I'll set each property with the respective setters like this:

$category = (new Category())
  ->setUuid(UuidGenerator::uuid4() )
  ->setName("Fiction");
Enter fullscreen mode Exit fullscreen mode

Yep. I have a Repository class for communicating with database, and that one will use a generic method that uses reflection to figure out the properties and methods to use for each property.

Collapse
adam_cyclones profile image
Adam Crockett

I'm forever walking into reflective doors 🥁

Collapse
andersbjorkland profile image
Anders Björkland Author

I didn't see that one coming 😉