Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generator with attributes #162

Open
flip111 opened this issue Feb 24, 2023 · 5 comments
Open

Generator with attributes #162

flip111 opened this issue Feb 24, 2023 · 5 comments

Comments

@flip111
Copy link

flip111 commented Feb 24, 2023

It would be cool when it's possible to define a default generator as php attribute. Haskell's quickcheck also allows you to define a default implementation through the Arbitrary typeclass.

@ilario-pierbattista
Copy link
Collaborator

Hi @flip111 could you provide a sample snippet? Would be helpful for reasoning about it.

@flip111
Copy link
Author

flip111 commented Feb 25, 2023

<?php

#[Attribute]
class IntegerGenerator {
  private int $min;
  private int $max;

  public function __construct(array $options) {
    $this->min = $options['min'] ?? PHP_INT_MIN;
    $this->max = $options['max'] ?? PHP_INT_MAX;

    if (isset($options['preset'])) {
      switch ($options['preset']) {
        case 'neg':
          $this->min = PHP_INT_MIN;
          $this->max = -1;
          break;
        case 'nat':
          $this->min = 0;
          $this->max = PHP_INT_MAX;
          break;
        case 'pos':
          $this->min = 1;
          $this->max = PHP_INT_MAX;
          break;
        default:
          throw new \InvalidArgumentException(sprintf('Preset "%s" is not one of %s', $options['preset'], implode(', ', ['neg', 'nat', 'pos'])));
      }
    }
  }

  public function generate() : int {
    return random_int($this->min, $this->max);
  }
}

class Dummy {
  #[IntegerGenerator(['preset' => 'neg'])]
  public int $negative;

  #[IntegerGenerator(['min' => 0, 'max' => 10])] 
  public int $custom;
}


$reflClass = new \ReflectionClass(Dummy::class);
$reflProps = $reflClass->getProperties();

$object = new Dummy();

foreach ($reflProps as $reflProp) {
  $reflAttrs = $reflProp->getAttributes();
  foreach ($reflAttrs as $reflAttr) {
    if ($reflAttr->getName() === 'IntegerGenerator') {
      $generator = $reflAttr->newInstance();
      break;
    }
  }

  $prop_name = $reflProp->getName();
  $object->$prop_name = $generator->generate();
}

var_dump($object);

I have set the properties directly because they are public. But one could also put attributes on the constructor parameters. Or use a hydrator if you want to write private properties.

You could also use symfony's expression language to build own generator specifications.

Or you could specify a function or static method to act as generator.

@ilario-pierbattista
Copy link
Collaborator

Hi @flip111 thank you for the snippet, but I was more interested in a sample usage .

Maybe you'd like to have a Generator<Dummy> that generates instances of the Dummy class?
If this is the case, in what would differ from a map-generator approch?

        Generators::map(
            function (array $args) {
                return new Dummy(...$args);
            },
            Generators::tuple(
                Generators::neg(),
                Generators::choose(0, 10)
            )
        )

@flip111
Copy link
Author

flip111 commented Mar 2, 2023

Hi @ilario-pierbattista let's compare differences and after talk about whether they are important or not important.

  1. Your source code and generators are in different files (when you follow PSR-4)
  2. Your code uses now 9 lines, but probably more as i guess some code was left out. Vs 2 lines additional to what you have to write anyway (the Dummy class). I'm talking about the lines #[IntegerGenerator(['preset' => 'neg'])] and the other one.
  3. You are obliged to write a generator, where as using reflection could provide a default implementation.

So now let's discuss the points.

  1. Functionality wise it doesn't matter at all. Though let's look at another project which uses separate configuration written in php or with attributes. As far as i know almost everybody prefers to use attributes (or the older annotations) because of convenience.
  2. I think it matters because you basically create a DSL that get rid of noise stripping it down to essentials parts.
  3. A default implementation is a really convenient thing to have. The original QuickCheck has it available as a auxiliary package. (Generics in haskell are more or less reflection abilities in other languages)

You could have 3 different levels for default implementation:

  1. Only when specified on class properties (as in the example above)
  2. When specified on the class. You can figure out which type properties need by reflection. For example for Int you can give PHP_INT_MIN to PHP_INT_MAX as default. And people can still use overrides on properties as with the first level.
  3. When given a configuration value to eris "make generators for all classes" then you can get rid of all attributes and it will generate an object graph for you out of nothing.

I hope that answers your question

@ilario-pierbattista
Copy link
Collaborator

Hi @flip111, sorry for being so late, thank you for your answer.

Let's recap some points:

  • Eris should provide an arbitrary generator method that takes as input the T class name, and creates a Generator<T>.
  • The arbitrary generator might be configured on class T using PHP attributes.
  • Eris should apply the arbitrary generator recursively on attributes typed as classes, in order to generate an object graph.
  • The arbitrary generator should be able to tackle also classes that are not configured via PHP attributes (or annotations) and infer from each attribute's type which is the correct generator to use.

Did I miss something?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants