0

I have a class that extends Eloquent\Model. I want to define a trait that adds a static property and a static method to the class.

trait Searchable
{
  protected static array $searchable;

  public static function formatSearchQuery($value)
  {
    $queryArray = array();
    $paramArray = array();

    foreach (self::$searchable as $field) {
      $queryArray[] = "{$field} LIKE ?";
      $paramArray[] = $value . '%';
    }

    return ['query' => join(" OR ", $queryArray), 'params' => $paramArray];
  }
}

the aim is to define the fields that can be used in a text search query.

The problem I have is when I define my class like this:

class Benefit extends NAFModel
{
  use Searchable;
  /**
   * Name for table without prefix
   *
   * @var string
   */
  protected $table = 'naf_agenda_benefits';

  protected static array $searchable =
  [
    'name',
    'description',
  ];
}

I get the following error :

PHP Fatal error: NafAgenda\Eloquent\Client and NafAgenda\Traits\Searchable define the same property ($searchable) in the composition of NafAgenda\Eloquent\Client. However, the definition differs and is considered incompatible. Class was composed in /var/www/html/wp-content/plugins/naf-agenda-react/includes/Eloquent/Client.php on line 9

T00rk
  • 2,178
  • 3
  • 17
  • 23
  • You may not initialize the array like you're doing... – Honk der Hase Aug 30 '23 at 19:04
  • How can I initialize it in my class definition ? – T00rk Aug 30 '23 at 19:05
  • I would write a method and call it from the constructor. – Honk der Hase Aug 30 '23 at 19:06
  • It is a static property – T00rk Aug 30 '23 at 19:07
  • Do you control the trait or is it third-party code you can't touch? I ask because https://stackoverflow.com/a/77011040/367456 . – hakre Aug 30 '23 at 19:50
  • 2
    Alternatively, defining the `$searchable` property in the trait doesn't really _do_ anything, and you _must_ declare it in the class definition anyway. You could simple remove it from the trait and add a guard clause in the method that uses it. – Sammitch Aug 30 '23 at 20:03
  • 1
    I'm also now with what @Sammitch wrote, I didn't see it after writing a first alternative example, that's pretty much the "By the way remark" below the example. One could even add an abstract base class in the mix that is defining it and then guard by type, not only by static property existince and there is some templating, which could also be helpful. – hakre Aug 30 '23 at 20:29
  • I agree with Sammitch too. – Mohammed Jhosawa Aug 30 '23 at 20:44

2 Answers2

1

If you're looking for reference, this is straight from the PHP Manual related to your question:

If a trait defines a property then a class can not define a property with the same name unless it is compatible (same visibility and type, readonly modifier, and initial value), otherwise a fatal error is issued.

This is bad news for you, as it means that you have written non-working code by violating these rules, in your case specifically the initial value:

trait Searchable
{
  protected static array $searchable;

vs.

class Benefit extends NAFModel
{
  protected static array $searchable =
  [
    'name',
    'description',
  ];

Therefore, you need to work around such a limitation (there is no conflict resolution possible, e.g. as for methods, cf. "Collisions with other trait methods").


I want to define a trait that adds a static property and a static method to the class.

As outlined, the two properties need to be compatible, therefore the resolution is to make them so.

Use the method aliased, define formatSearchQuery, then when called, initialize the properties value you'd like to override and then delegate to the alias.

If you also want to have an initial value, add a new static property for that and use it in your defined formatSearchQuery to copy the value over.

class Benefit extends NAFModel
{
    use Searchable {formatSearchQuery as _formatSearchQuery;}

    /**
     * Name for table without prefix
     *
     * @var string
     */
    protected $table = 'naf_agenda_benefits';

    protected static array $_searchable =
        [
            'name',
            'description',
        ];

    public static function formatSearchQuery($value)
    {
        # lazy initialization pattern, only _one_ example.
        self::$searchable ??= self::$_searchable;

        return self::_formatSearchQuery($value);
    }
}

By the way, if you control the Trait, you can design the mixin already with extension in mind, e.g. the original implementation of formatSearchQuery() in the trait could handle that.


However you have not shared what you're trying to achieve therefore there is little suggestion to be given. Not making it static would not solve the class composition problem on the Traits level. You perhaps have an initialization problem and you try to solve it by static, perhaps you're looking for a static factory method and then have your system handle the details per instance.

Try to build your class hierarchy with abstract super-classes only first and then add traits only for additional mixins not crossing existing names.

Commonly its known that composition should be favored over inheritance, and traits likely fall into the later category than the first. YMMV.

hakre
  • 193,403
  • 52
  • 435
  • 836
  • I am inclined towards your solution. Thanks! – Mohammed Jhosawa Aug 30 '23 at 20:28
  • 1
    @MohammedJhosawa: Can you say if you can edit the trait? Because if so, you don't need any implementation for classes using it. See also the comment by user Sammitch. – hakre Aug 30 '23 at 20:31
  • I have edited the solution based on Sammitch comment but haven't added a safe guard to `formatSearchQuery` method – Mohammed Jhosawa Aug 30 '23 at 20:42
  • 1
    A simple save guard would be the property exists and otherwise throw: `foreach (self::$searchable ?? throw new Error(self::class . '::$searchable not iterable') as ...` -- here it's inlined, but it should give you an idea. If it is otherwise not iterable (here a null check only, cf. [_null coalescing operator_](https://php.net/language.operators.comparison#language.operators.comparison.coalesce), foreach on it's own will complain on any other cases if not iterable). – hakre Aug 30 '23 at 20:54
  • Maybe before `foreach` only it should check if the property exists or not in the class – Mohammed Jhosawa Aug 30 '23 at 21:01
  • Yes, clearly better, I was just quickly pulling in an example into the comment. I would actually put it in front and also extend the error message to give better guidance, that message would be pretty abstract which might not be helpful when you run into this error in two weeks from now. – hakre Aug 30 '23 at 21:18
0

If a trait defines a property then a class can not define a property with the same name unless it is compatible (same visibility, type, readonly modifier and initial value), otherwise a fatal error is issued. (Reference from PHP Manual)

Methods defined in traits can access methods and properties of the class, therefore we don't need to define searchable array in the Trait.

<?php

trait Searchable
{
    public static function formatSearchQuery($value)
    {
        $queryArray = array();
        $paramArray = array();

        if (!is_array(self::$searchable ?? null)) {
          throw new Error(self::class . '::$searchable not iterable');
        }

        foreach (self::$searchable as $field) {
            $queryArray[] = "{$field} LIKE ?";
            $paramArray[] = $value . '%';
        }

        return ['query' => join(" OR ", $queryArray), 'params' => $paramArray];
    }

}

class Benefit extends NAFModel
{
    use Searchable;

    protected static array $searchable = ['name', 'description'];

    /**
     * Name for table without prefix
     *
     * @var string
     */
    protected $table = 'naf_agenda_benefits';
}

print_r(Benefit::formatSearchQuery('Something'));
  • 1
    suggestion fyi only: `if (!is_array(self::$searchable ?? null)) { ...` if the original intend is to have an array only at that place (which IMHO makes sense as it is used as a data-holder only in this early form). Depends on the use-case, but I would not check for property existence but just use null coalescing if it's nowadays PHP code: more speaking (and actually also less to do/type). – hakre Aug 30 '23 at 21:26