Is it possible to define a public method in a base class without limit the parameter's type and how many parameters input?

I wanna to define a base class for all my db table class. It is like the following:

namespace Application\Model\LocalBase;


use Laminas\Db\TableGateway\TableGatewayInterface;

class TableBase
{
    protected TableGatewayInterface $tableGateway;

    public function __construct(TableGatewayInterface $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    public function fetchRecord($whereArr)
    {
        $rowset = $this->tableGateway->select($whereArr);
        return $rowset->current();
    }

    public function deleteRecord($whereArr)
    {
        return $this->tableGateway->delete($whereArr);
    }

    /** this method I wanna just limit the fuction name, no limit to the parameters */
    public function getPaginator(...$args)
    {
        //
    }

    public function insertRecord(array $data): int
    {
        return $this->tableGateway->insert($data);
    }

    public function updateRecord(array $whereArr,array $data): int
    {
        return $this->tableGateway->update($data,$whereArr);
    }
    
    /** this method, just limit func name, no limit to parameters, one or two or three, whatever the parameter type is */
    public function saveRecord(...$args)
    {
        //
    }
}

When I extend the above base class. I wanna it looks like.

namespace Member\Model;

use Application\Model\LocalBase\TableBase;
use Application\Model\UtilsFuncs;
use Laminas\Http\Request;

class MessageboardTable extends TableBase
{
    // when I overwrite this method like this, the phpstorm editor raise error, not compatible...
    public function saveRecord(Messageboard $messageboard, UtilsFuncs $utilsFuncs, Request $request)
    {
        $ip = $utilsFuncs->getIp($request);
        if (!$ip) return $utilsFuncs->uniformJson(false, 'can not get ip');

        // ... save data into database logic here...
    }
}

Is it possible to reach such requirement? Base class limit the function name, and make other workmate not to name the saving record logic fucntion. But it won’t limit the parameter. And allow the sub class who extent the base class can format the input parameter’s type. Like the above MessageboardTable class, the first parameter must be Messageboard entity.

Or it is possible to fulfil. Any kind people can give me a certain answer?

Hi @jobsfan,

If my memory serves me correctly, it is possible via an interface. The example can be found here. Validator isValid method accepts one parameter according to the interface but multiple can be passed. I’ve not implemented this myself, therefore, I can’t endorse it. Thanks!

I look via the file Input.php

And track it as

$validator = $this->getValidatorChain();
$result    = $validator->isValid($value, $context);

And find $validator = $this->getValidatorChain(); here

public function getValidatorChain()
{
    if (! $this->validatorChain) {
        $this->validatorChain = new ValidatorChain();
    }
    return $this->validatorChain;
}

Then $this->validatorChain = new ValidatorChain();
Found ValidatorChain.php 's isValid()

public function isValid($value, $context = null)
{
    $this->messages = [];
    $result         = true;
    foreach ($this as $element) {
        $validator = $element['instance'];
        assert($validator instanceof ValidatorInterface);
        if ($validator->isValid($value, $context)) {
            continue;
        }
        $result         = false;
        $messages       = $validator->getMessages();
        $this->messages = array_replace($this->messages, $messages);
        if ($element['breakChainOnFailure']) {
            break;
        }
    }
    return $result;
}

The class ValidatorChain.php implements ValidatorInterface
there is

interface ValidatorInterface
{
    /**
     * Returns true if and only if $value meets the validation requirements
     *
     * If $value fails validation, then this method returns false, and
     * getMessages() will return an array of messages that explain why the
     * validation failed.
     *
     * @param  mixed $value
     * @return bool
     * @throws Exception\RuntimeException If validation of $value is impossible.
     */
    public function isValid($value);

    /**
     * Returns an array of messages that explain why the most recent isValid()
     * call returned false. The array keys are validation failure message identifiers,
     * and the array values are the corresponding human-readable message strings.
     *
     * If isValid() was never called or if the most recent isValid() call
     * returned true, then this method returns an empty array.
     *
     * @return array<string, string>
     */
    public function getMessages();
}

You are right, so I need to create a interface instead of the base class TableBase. right?

But If I make a interface, all the method must be overwirte in the child class?

It seems not work

Firstly I create a interface like

namespace Application\Model\LocalBase;

interface TableBaseInterface
{
    /** 获取分页器
     * @param mixed $args
     * @return mixed
     */
    public function getPaginator($args);

    /** 保存记录
     * @param mixed $args
     * @return mixed
     */
    public function saveRecord($args);
}

Then I made a Trait like

namespace Application\Model\LocalBase;

use Laminas\Db\TableGateway\TableGatewayInterface;

trait TableBaseTrait
{
    protected TableGatewayInterface $tableGateway;

    public function __construct(TableGatewayInterface $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    public function fetchRecord($whereArr)
    {
        $rowset = $this->tableGateway->select($whereArr);
        return $rowset->current();
    }

    public function deleteRecord($whereArr)
    {
        return $this->tableGateway->delete($whereArr);
    }

    public function insertRecord(array $data): int
    {
        return $this->tableGateway->insert($data);
    }

    public function updateRecord(array $whereArr,array $data): int
    {
        return $this->tableGateway->update($data,$whereArr);
    }
}

Error raise in the phpstorm ide

Hi there!
Your initial approach already did look good, but if you want method parameter overloading in PHP, I am afraid I have to disappoint you…
This is another rather hacky approach without strict typing like you want.

Define the method in TableBase like this:

    public function saveRecord()
    {
/* now do stuff, throw NotImplementedException, something. */
    }

then, in MessageboardTable, do this:

public function saveRecord()
    {
          $args = func_get_args();
          // extract the passed parameters from args, typehint them, do stuff
    }

fun_get_args()-doc

You’re probably better off using the ...-spread operator like you did.

@jobsfan, Thanks for the reply. I’ll check myself how this magic works with Validators. It is a good thing to learn. Your reply is much appreciated.

Another way you can accomplish this in php 8.1 or newer would be to provide the method signatures you wish to change as private methods and provide a getter for those methods. Private methods do not have to match the parents signature. If you provide a getter for the method in 8.1 you can then use first class callable syntax to call it.

Please see the following for details:
https://www.php.net/manual/en/language.oop5.basic.php
https://www.php.net/manual/en/functions.first_class_callable_syntax.php
https://www.php.net/manual/en/language.oop5.variance.php

That’s too ugly to use $args = func_get_args();

yes, I am using php 8.1, I just want to guide my workmate to name the class method with a uniform name. And found it is very hard to reach this target.

Have you read the related docs I linked?

If this method is declared in 8.1 as
private method saveRecord() {}

and you provide a getter for the method:

    private function getSaveRecord() {
        return __METHOD__;
    }

Then you can then call it like:

$saveRecord = $yourInstance->getSaveRecord(// your args);
$returned = $saveRecord($argOne, $argTwo);

The docs I linked provides all the explanation that is required. getSaveRecord could actually be declared in a trait and used in a abstract class that all other classes extend.

@jobsfan, The thing you’re trying to achieve can only be achieved by the hack @ @Tyrsson has suggested. The reason is you’re asking for polymorphism in PHP, which is impossible by design. PHP doesn’t allow you to have the same method name with different arguments. I hope this helps. Thanks!

The relevant part from one of the pages I linked.

From the manual:

Blockquote
When overriding a method, its signature must be compatible with the parent method. Otherwise, a fatal error is emitted, or, prior to PHP 8.0.0, an E_WARNING level error is generated. A signature is compatible if it respects the variance rules, makes a mandatory parameter optional, adds only optional new parameters and doesn’t restrict but only relaxes the visibility. This is known as the Liskov Substitution Principle, or LSP for short. The constructor, and private methods are exempt from these signature compatibility rules, and thus won’t emit a fatal error in case of a signature mismatch.

I posted the solution that I did because it allows maintaining required arguments.

Thanks, @Tyrsson for sharing yet another useful information. Can you suggest any book on Aspect Oriented Programming? But do you agree that polymorphism can’t be achieved in PHP, due to the fact it doesn’t let you have the same methods with different arguments? Thanks!

Since @Tyrsson is right with overriding method signatures and the associated restrictions on the part of PHP I feel a bit uncomfy with the topic itself. Isn 't it a bad design and shouldn 't we give advice like solid principles: single responsibility?

One can not code the jack of all trades. This will always end up in a complete mess.

SOLID: Single Responsibility

Instead of overriding method signatures, think of a way to map different prerequisites for the process. Wouldn’t a single object for each different data set to be saved be the more logical consequence? Think of the following interface.

<?php

declare(strict_types=1);

namespace Marcel\Model;

interface TableInterface
{
    public function saveRecord(RecordSaverInterface $saver): mixed;
}

And the record saver interface, which only needs the table gateway instance.

<?php

declare(strict_types=1);

namespace Marcel\Model;

use Laminas\Db\TableGateway\TableGatewayInterface;

interface RecordSaverInterface
{
    public function setTableGateway(TableGatewayInterface $tableGateway): void;
}

The RecordSaverInterface interface can contain all the data you need to save your data set. In a concrete implementation, you can implement the corresponding properties of the object.

A concrete Implementation

Since we 've defined an interface for saving objects it will be pretty easy to code an implementation.

<?php

declare(strict_types=1);

namespace Marcel\Model;

use Laminas\Db\TableGateway\TableGatewayInterface;

abstract class AbstractTable implements TableInterface
{
    public function __construct(
         protected TableGatewayInterface $tableGateway
    ) {}

    public function saveRecord(RecordSaverInterface $saver): mixed
    {
        $saver->setTableGateway($this->tableGateway);
        return $saver();
    }
}

The abstraction is easier to test than the implementation of a trait. If you really want to, you can of course also use a trait instead of an abstract class.

<?php

declare(strict_types=1);

namespace Marcel\Model;

class MessageboardRecordSaver implements RecordSaverInterface
{
    protected TableGatewayInterface $tableGateway;

    public function __construct(
        protected Messageboard $messageboard,
        protected UtilsFuncs $utilsFuncs,
        protected Request $request
    ) {}

    public function __invoke()
    {
         // just do the relevant stuff for saving messageboard datasets
    }

    public function setTableGateway(TableGatewayInterface $tableGateway): void
    {
         $this->tableGateway = $tableGateway;
    }
}

… and finally the MessageboardTable class.

<?php

declare(strict_types=1);

namespace Marcel\Model;

class MessageboardTable extends AbstractTable
{
    
}

Because the saveRecord method is already defined in the abstract class, you no longer need to define it in your class.

$recordSaver = new MessageboardRecordSaver(
    $messageboard,
    $utils,
    $request
);

$table = new MessageboardTable($tableGateway);
$record = $table->saveRecord($recordSaver);

With the above shown example you can code implementations with not being limited by dependencies. Each implementation can have as many dependencies as it needs. No method signature has to be overwritten. Everything is clearly separated and fulfils its purpose.

Isn’t the approach shown here less painful and easier to implement than frantically trying to change the signature of a method?

@ezkimo, I just want to know in a single sentence that the usage of polymorphism is condemned by Solid Single Responsibility. As I’m just a developer, code monkey to be specific. Haha! Thanks!

As long as you recognize @Tyrsson 's advices polymorphism is kinda limited possible. You will reach your limits relatively quickly when it comes to type hinting, for example. I would always prefer single responsibility, because it 's simple and stupid and gives you all that you need without the headache.

You know, I 'm just a simple dev. :wink:

1 Like

@ezkimo, I loved your diplomatic answer.

I would say a simple diplomatic dev. :wink:

Can you recommend books on Aspect Oriented Programing? Thanks!

Oh, please do not get me wrong. I was in no way endorsing the approach. I was simply addressing the question at hand.

I generally handle it this way in most of my projects (with similar code, this is some code I was working on to possibly improve previous approaches). CommandBus implementation, Laminas-Db. Pretty standard stuff. The below code is some I was testing for a new project that I recently started but had to put on hold.

<?php

declare(strict_types=1);

namespace PageManager\Storage;

use Webinertia\Db\EntityInterface;

final class SavePageCommandHandler
{
    public function __construct(
        private PageRepository $storage
    ) {
    }

    public function handle(SavePageCommand $command): EntityInterface|int
    {
        return $this->storage->save($command->entity);
    }
}

The underlying save method

    public function save(EntityInterface $entity): EntityInterface|int
    {
        $set = $this->hydrator->extract($entity);
        if ($set === []) {
            throw new \InvalidArgumentException('Repository can not save empty entity.');
        }
        try {
            if (! isset($set['id']) ) {
                // insert
                $this->gateway->insert($set);
                $set['id'] = $this->gateway->getLastInsertValue();
            } else {
                 $this->gateway->update($set, ['id' => $set['id']]);
            }
        } catch (\Throwable $th) {
            // todo: add logging, throw exception
        }
        return $this->hydrator->hydrate($set, $entity);
    }
1 Like

About as close as you are going to get in Laminas/Mezzio is via delegator usage.

the above code is wrapped with a saveRecord() fucntion in the extend sub class?

like:

class MessageboardTable extends TableBase
{
    // when I overwrite this method like this, the phpstorm editor raise error, not compatible...
    public function saveRecord(Messageboard $messageboard, UtilsFuncs $utilsFuncs, Request $request)
    {
        $saveRecord = $this->getSaveRecord(// your args);
        $returned = $saveRecord($argOne, $argTwo);
    }
}

I dont’ know how it works :frowning:

What I wanna to restrict the function name for the saving logic. If my workmate extent the tablebase class and he wanna to do a save logic. when he type a save prefix in the ide like phpstorm. the editer will autocomplete a saveRecord() for him.

So it must have a saveRecord() function inside the sub extend class MessageboardTable, if this is a before set condition, I have to wrap your code like $saveRecord = $yourInstance->getSaveRecord(// your args); $returned = $saveRecord($argOne, $argTwo); inside my MessageboardTable class, then it should be $saveRecord = $this->getSaveRecord(// your args); $returned = $saveRecord($argOne, $argTwo);, right? it sound like a recycle(recursive function)!

I am not sure my understanding of your suggestion above is correct or not.