DEV Community

Cover image for From object graph to query
david duymelinck
david duymelinck

Posted on

From object graph to query

In my previous post I mentioned thinking about generating a query from code. And my example was the following.

new Query(
  new Select(
    new Fields(
      new Field(Pages::Id),
      new Field(Pages::Name),
      new Field(Pages::Url),
    ),
    new From(Pages::Pages),
    new Where(
      new EqualParameter(Pages::Id),
    )
  )
)
Enter fullscreen mode Exit fullscreen mode

To get it out of my mind, I created a fast solution using the Stringable interface.

The base classes

abstract readonly class StringMultiple implements Stringable
{
  protected array $classes;

  public function __construct(Stringable ...$classes)
  {
    $this->classes = $classes;
  }

  public function __toString(): string
  {
    $classes = array_map(fn ($class) => (string) $class, $this->classes);

    return join(' ', $classes);
  }
}
Enter fullscreen mode Exit fullscreen mode

This is the base class for all classes that can contain other classes.
The beauty of this is that PHP takes care of the deeper levels on the graph by calling the __toString method on every class.

abstract readonly class StringSingle implements Stringable
{
    public abstract function __toString(): string;
}
Enter fullscreen mode Exit fullscreen mode

This is the base class for classes that don't contain other classes.
Most of the time these classes will have a different constructor.

The query classes

readonly class Query extends StringMultiple
{}
Enter fullscreen mode Exit fullscreen mode

The Query class is there to start building the string. That is why it has nothing in the body of the class.

readonly class Select extends StringMultiple
{
    public function __toString(): string
    {
        return 'SELECT ' . parent::__toString();
    }
}
Enter fullscreen mode Exit fullscreen mode

This adds SELECT in front of the rest of the string.

readonly class Fields extends StringMultiple
{
  public function __toString(): string
  {
     $fields = array_map(fn ($class) => (string) $class, $this->classes);

     return join(', ', $fields);
  }
}
Enter fullscreen mode Exit fullscreen mode

The fields class takes care of the comma's between the fields.

readonly class Field extends StringSingle
{
    public function __construct(private Identifier $field)
    {}

    /**
     * @inheritDoc
     */
    public function __toString(): string
    {
        return queryStringFromIdentifier($this->field);
    }
}
Enter fullscreen mode Exit fullscreen mode

I'm using Identifier and queryStringFromIdentifier from the Idable queries core library to transform the enum case to the database field.

readonly class From extends StringSingle
{
  public function __construct(private Identifier $field)
  {}

  public function __toString(): string
  {
    return 'FROM ' . queryStringFromIdentifier($this->field);
  }
}
Enter fullscreen mode Exit fullscreen mode

The same Identifier code, with the addition of prefixing the string with FROM.

readonly class Where extends StringMultiple
{
  public function __toString(): string
  {
     return 'WHERE ' . parent::__toString();
  }
}
Enter fullscreen mode Exit fullscreen mode

I guess you can already see the pattern at this point.

readonly class EqualParameter extends StringSingle
{
  public function __construct(private Identifier $identifier)
  {}

  public function __toString(): string
  {
     $querystring = queryStringFromIdentifier($this->identifier);

     return "$querystring = :$querystring";
  }
}
Enter fullscreen mode Exit fullscreen mode

Now I have all the classes to create the query I showed in the example.
So all that it needs to get the query as a string is to add (string) in front of the object graph.

Of course there are still a lot of things that need to be done to have all the options SQL provides.
But now I won't give it as much thought as I did before. And I can focus on the other database packages for the Idable queries library family.

Top comments (0)