static property not initialised when constructor is called php

231 views Asked by At

I have a parent class like so:

abstract class UiElement
{
    protected static ?string $template_name = null;

    public function __construct() {
        if(static::$template_name == null) {
            throw new Exception("static \$template_name has not been set in child class", 1);
        }
    }
}

now i have a child class like so:

class EmptyContent extends UiElement
{
    protected static ?string $template_name = 'empty-content';

    public function __construct() {
        parent::__construct();
    }
}

and call it like so:

$empty = new EmptyContent();

I want to make sure the child class in this case EmptyContent sets a value other than null when defining this class. So i do the check in the parent's class constructor but this gives me the following error:

Fatal error: Uncaught Exception: static $template_name has not been set in child class in /Applications/MAMP/htdocs/private_projects/Foodio/App/index.php:108 Stack trace: #0 /Applications/MAMP/htdocs/private_projects/Foodio/App/index.php(126): UiElement->__construct() #1 /Applications/MAMP/htdocs/private_projects/Foodio/App/index.php(134): failure->__construct() #2 {main} thrown in

As far as i understand this is due to the fact that the property $template_name is not yet initialised when the constructor of the parent is fired.

How do i go about doing this?
If more information or clarification is needed let me know so i can add it!

2

There are 2 answers

1
El_Vanja On BEST ANSWER

You could create an abstract method instead of a property:

abstract class UiElement
{
    abstract protected static function getTemplateName(): string;
}

class EmptyContent extends UiElement
{
    protected static function getTemplateName(): string
    {
        return 'empty-content';
    }
}

This way you cannot create a child class without the implemented method (forcing a definition). Should you create a child without it:

class EmptyContent extends UiElement
{
}

PHP will throw a fatal error before running your script: Class EmptyContent contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (UiElement::getTemplateName).

Now, this comes with its drawbacks. It won't protect you against implementing an empty method or one that returns a wrong type. That kind of stuff will only explode in your face once you use it. You could protect against that by adding a constructor to the child (additionally checking that you don't return an empty string):

class EmptyContent extends UiElement
{
    public function __construct()
    {
        if (strlen(self::getTemplateName()) === 0) {
            throw new Exception('Template name is not defined');
        }
    }

    ...

in order to force the check on instantiation. This would have to be added on every child (tedious and WET), since you can't handle it on parent level (that would try to call its abstract method and fail before reaching runtime).

There's also another approach you could take, making it a regular (non-abstract) method that throws in the parent:

abstract class UiElement
{
    protected function getTemplateName(): string
    {
        throw new Exception('Method not implemented');
    }
}

This will also fail only when you attempt to use the method (not during instantiation) on a child that didn't implement the method, but would be handled in one place, inside the parent. Up to you to weigh the pros and cons.

P.S. Not sure if the method really needs to be static if it's only going to be used internally, but you know your reasoning better than I do.

0
Héctor Paúl Cervera-García On

As suggested, you could use an abstract method to force implementing classes to define it:

abstract class UiElement
{
    abstract protected function getTemplateName(): string;
}

Then, implementing classes should define what string to return:

class EmptyContent extends UiElement
{
    protected function getTemplateName(): string
    {
        return 'empty-content';
    }
}

This means that everywhere you want to use this value, you have to retrieve it by calling $this->getTemplateName().