@method: methods that we hate to write

7 years ago

David Grudl
Nette Core | 6886
+
+2
-

I implemented to Nette\Object support for dynamically created methods declared using annotations. It is syntactic sugar for methods that we do not like to write. Useful and intended for data transfer objects or so-called entities.

Let's have simple entity:

class Entity extends Nette\Object
{
    /** @var string */
    public $name;

    /** @var bool */
    public $enabled;
}

Nice, but there is no encapsulation. So let's change public to private:

class Entity extends Nette\Object
{
    /** @var string */
    private $name;

    /** @var bool */
    private $enabled;


    public function setName(string $name) // hmmm, this is maybe PHP 6.0 typehint
    {
        $this->name = (string) $name;
        return $this;
    }

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

    public function setEnabled(bool $on)
    {
        $this->enabled = (bool) $on;
        return $this;
    }

    public function isEnabled()
    {
        return $this->enabled;
    }

}

There are 4 boring methods! And typehints used by setters, they are still PHP wet dream (and I think it is absolutely incomprehensible lack of PHP).

Let's Nette\Object generate these methods:

/**
 * @method setName(string)
 * @method string getName()
 * @method setEnabled(bool)
 * @method bool isEnabled()
 */
class Entity extends Nette\Object
{
    /** @var string */
    private $name;

    /** @var bool */
    private $enabled;
}

$obj = new Entity;
$obj->setName('hello'); // it works!

There is special support for arrays:

/**
 * @method addItem(string)
 * @method setItems(string[])
 * @method string[] getItems
 */
class Entity extends Nette\Object
{
    protected $items;
}

$obj = new Entity;
$obj->addItem('hello');

It is useful to declare typehints directly in @method annotations, but if you omit them, @var annotations are good enough:

/**
 * @method addItem
 * @method setItems
 * @method getItems
 */
class Entity extends Nette\Object
{
    /** @var string[] */
    protected $items;
}

Of course, all these typehints are taken into consideration:

// there is typehint string[]
$obj->setItems(array(1, 2));
dump($obj->getItems()); // prints array of *strings* array('1', '2');

// or
$obj->setItems(1); // throws: Argument passed to Entity::setItems() must be string[], integer given

Magic getters and setters act like normal methods. So you can use Nette\Object features with them. Eg. you can store method to variable:

$obj = new Entity;
$method = $obj->addItem;
$method('abc');

and first of all, you can count with well known magic properties!

$items = $obj->items; // calls Entity::getItems()

Magic properties for magic methods, it's magic2.

Enjoy it!

7 years ago

pawouk
Member | 171
+
0
-

Nice one!

public function setName(string $name) // yes, this is maybe PHP 6.0 typehint
    {
        $this->name = (string) $name;
        return $this;
    }

So what about to create simple object like Nette\String, Nette\Int … than you need not to wait to PHP 6. But it maybe turn aplication slow. And basic function like is_scalar, strpos atd will not work :-(

7 years ago

duke
Member | 650
+
0
-

@pawouk I don't think David meant he has to wait for PHP 6, but rather that his new toy can do (through some magic) something PHP will maybe support natively in version 6.

Last edited by duke (2013-05-06 03:40)

7 years ago

Jan Tvrdík
Nette guru | 2563
+
0
-

@David Grudl: How does this implementation affects performance of all current classes using Nette\Object?

7 years ago

David Grudl
Nette Core | 6886
+
0
-

Props are about cca 5 % slower, in real life undetectable.

7 years ago

frosty22
Member | 373
+
0
-

Well, i like because setters/getters method are about 70% content of all my classes :( I generate it via IDE, but still there are. I use something like this for Doctrine entities already (https://github.com/…seEntity.php).

Last edited by frosty22 (2013-05-06 09:14)

7 years ago

nAS
Member | 279
+
0
-

Don't forget that many users use @method as a regular comment to enable autocomplete in IDE.

Typical example are extension-methods on FormContainer and @method annotations on Form.

7 years ago

_Martin_
Generous Backer | 680
+
0
-

Hmmm, I can't help myself: what about language that compiles into PHP? Something like Live Script for JavaScript. Nette\Object can do so many things, so – in my opinion – it should be the next logical step. And it would be even faster than now ;)

7 years ago

Jan Tvrdík
Nette guru | 2563
+
0
-

@_Martin_: Feel free to implement it as RobotLoader's filter.

7 years ago

vrana
Member | 130
+
0
-

I suppose that $obj->name = 'a' works and checks the type thanks to another magic property of Nette\Object (getters and setters allow access through variable), right? It could be a little confusing that you can access a private property but it seems like the right behavior.

The only thing that probably wouldn't work is $obj->items[] = 1. Or were you able to circumvent this problem (“Indirect modification of overloaded property”) somehow too?

7 years ago

Majkl578
Moderator | 1379
+
0
-

Few more things:

  • Why don't we support indexed arrays (like e.g. 2nd argument being a key)?
  • Why don't we support removers? Having adders implicates having also removers.
  • Why throwing an exception if more parameters is given? It's not an error in PHP itself.

Additionally, I don't think this should be part of Object itself, rather a separate trait, but that's a problem until traits are supported.

vrana wrote:

It could be a little confusing that you can access a private property but it seems like the right behavior.

Agreed. It might seem really strange, using two magics at once.

6 years ago

kravčo
Member | 723
+
0
-

Nette Framework – no wtf, just magic squared!

6 years ago

Tharos
Member | 1042
+
0
-

add<something> doesn't respect English grammar.

elf = elves (not elfs)
calf = calves (not calfs)
knife = knives (dtto)
loaf = loaves
shelf = shelves
wolf = wolves
loaf = loaves
man = men
person = people
mouse = mice
child = children
foot = feet
goose = geese
tooth = teeth
louse = lice
cactus = cacti
appendix = appendices
ox = oxen

Last edited by Tharos (2013-06-12 13:04)

6 years ago

duke
Member | 650
+
0
-

I've noticed that if I use @method property to define a setter, autowiring in config won't work for that setter (I have to explicitly specify the argument). Is that a bug or intended behavior?

E.g. something like:

namespace MyApp\AdminModule;

use MyApp;

/**
 * Authentication presenter.
 * @method setUserManager(MyApp\UserManager)
 */
class AuthPresenter extends BasePresenter
{
    /** @var MyApp\UserManager */
    private $userManager;

    ...
}
services:
    UserManager:
        class: MyApp\UserManager

    AdminAuthPresenter:
        class: MyApp\AdminModule\AuthPresenter
        setup:
            - setUserManager # this will result in Nette\InvalidArgumentException: MyApp\AdminModule\AuthPresenter::setUserManager() expects 1 argument, 0 given.
            # - setUserManager(@UserManager) # this would work

5 years ago

Tomáš Votruba
Moderator | 1154
+
0
-

@duke This is intended behavior. Setters are not autowired.

And this is typehint control:

/**
 * @method setUserManager(MyApp\UserManager)
 */

5 years ago

duke
Member | 650
+
0
-

Tomáš Votruba wrote:

@duke This is intended behavior. Setters are not autowired.

Setters of course are autowired, but I asked about magical setters defined via @method (those currently aren't autowired, and I was wondering if it is by design).

5 years ago

Tomáš Votruba
Moderator | 1154
+
0
-

@duke Didn't know that. Where is that behavior in the code?

So this would work with hand-written setter?

services:
    UserManager:
        class: MyApp\UserManager

    AdminAuthPresenter:
        class: MyApp\AdminModule\AuthPresenter
        setup:
            - setUserManager

Last edited by Tomáš Votruba (2014-07-19 23:52)

5 years ago

duke
Member | 650
+
0
-

Autowiring of arguments for regular methods seems to be handled by this code. I don't know how exactly the magical setters are handled, but most likely the cause for autowiring not working for them is the lack of support in relevant reflection classes providing parameters info for these methods.

And answer to your 2nd question is yes.

Last edited by duke (2014-07-20 12:04)

5 years ago

David Grudl
Nette Core | 6886
+
0
-

Missing ReflectionMethod can be “mocked” this way https://api.nette.org/…der.php.html#550

5 years ago

Tomáš Votruba
Moderator | 1154
+
0
-

@duke Thanks for explaining, I'm bit smarter now :).

What do you recommend based on your practice?

  1. magic methods with neon configuration or
  2. hand written method with autowiring?

5 years ago

Majkl578
Moderator | 1379
+
0
-

David Grudl wrote:

Missing ReflectionMethod can be “mocked” this way https://api.nette.org/…der.php.html#550

Using eval + lambda function is faster and has significantly smaller memory footprint (see here create_function and here eval + lambda), also create_function is sometimes disabled on shared hostings for so-called security purposes.

5 years ago

duke
Member | 650
+
0
-

@TomášVotruba I have no preference. Either is good, but I think the autowiring should work even for the magical setters.

David Grudl's comment even suggests he knows how to make it work, but he probably expects someone else to actually implement it. :-)