4

I have a class that is using the State pattern. Here's a simple example

/**
 * @Enitity
 **/
class Door
{
  protected $id;
  protected $state;
  public function __construct($id, DoorState $state)
  public function setState(DoorState $state)
  {
    $this->state = $state;
  }
  public function close()
  {
    $this->setState($this->state->close())
  }
  ...
}

interface DoorState
{
  public function close;
  public function open;
  public function lock;
  public function unlock;
}

class DoorAction implements DoorState
{
  public function close()
  {
     throw new DoorError();
  }
  ...
}

then several classes that define the appropriate actions in the states

class OpenedDoor extends DoorAction
{
  public function close()
  {
      return new ClosedDoor();
  }
}

So I would have some thing like

$door = new Door('1', new OpenedDoor());
DoctrineDoorRepository::save($door);
$door->close();
DoctrineDoorRepository::save($door);

How would I implement the mapping in Doctrine so I can persist it? I'm hung up on the $state property. I would like to save the whole DoorAction based object but do I have to the map the DoorAction super class or each individual sub class?

I've looked at implementing it using Embeddable or SuperMapping but run into problems with each.

Clutch
  • 7,404
  • 11
  • 45
  • 56

2 Answers2

4

Doctrine2 DBAL has a feature in the documentation that allows ENUM's

https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/mysql-enums.html#mysql-enums

When we take the Solution 2: Defining a Type as a base, one could create an own type, for instance called doorstatetype or similar to represent the open/closed state. For instance like this:

<?php
namespace Acme\Model\Door;

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;

class DoorStateType extends Type
{
    const ENUM_DOORSTATE = 'enumdoorstate';
    const STATE_OPEN = 'open';
    const STATE_CLOSED = 'closed';

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        return "ENUM('" . self::STATE_OPEN . "', '" . self::STATE_CLOSED . "') COMMENT '(DC2Type:" . ENUM_DOORSTATE  . ")'";
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return $value;
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if (!in_array($value, array(self::STATE_OPEN, self::STATE_CLOSED))) {
            throw new \InvalidArgumentException("Invalid state");
        }
    
        return $value;
    }

    public function getName()
    {
        return self::ENUM_DOORSTATE;
    }
}

And then use it like this:

<?php

namespace Acme\Model\Door;

/** @Entity */
class Door
{
    /** @Column(type="enumdoorstate") */
    private $state;

    public function open()
    {
        if (!DoorStateType::STATE_OPEN === $this->state) {
            throw new \LogicException('Cannot open an already open door');
        }

        $this->state = DoorStateType::STATE_OPEN;
    }

    public function close()
    {
        if (!DoorStateType::STATE_CLOSED === $this->state) {
            throw new \LogicException('Cannot close an already closed door');
        }

        $this->state = DoorStateType::STATE_CLOSED;
    }
}

This allows searching for states:

$openDoors = $repository->findBy(array('state' => DoorStateType::STATE_OPEN));

You could basically then have the convertToPHPValue method create objects of the desired states that allow for some logic, like checking if an open door can be locked or similar.

In the case where the state has to be a class that contains logic, you could implement it like this:

First we define a normal state from which we can inherit:

<?php
namespace Acme\Model\Door;

abstract class DoorState
{
    // Those methods define default behaviour for when something isn't possible
    public function open()
    {
        throw new \LogicException('Cannot open door');
    }

    public function close()
    {
        throw new \LogicException('Cannot close door');
    }


    abstract public function getStateName();
}

Then the OpenState:

<?php
namespace Acme\Model\Door;

class OpenState extends DoorState
{
    const STATE = 'open';
    
    public function close()
    {
        return new ClosedState();
    }

    public function getStateName()
    {
        return self::STATE;
    }

    // More logic
}

And finally the ClosedState:

<?php
namespace Acme\Model\Door;

class ClosedState extends DoorState
{
    const STATE = 'closed';
    
    public function open()
    {
        return new OpenState();
    }

    public function getStateName()
    {
        return self::STATE;
    }

    // More logic
}

We can then, for persistence, simply use different convert methods:

<?php
namespace Acme\Model\Door;

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;

class DoorStateType extends Type
{
    // SQL declarations etc.

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if ($value === OpenState::STATE) {
            return new OpenState();
        }

        if ($value === ClosedState::STATE) {
            return new ClosedState();
        }

        throw new \Exception(sprintf('Unknown state "%s", expected one of "%s"', $value, implode('", "', [OpenState::STATE, ClosedState::STATE])));
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        return $value->getStateName();
    }
}
thormeier
  • 672
  • 9
  • 25
  • 1
    I really appreciated your approach here :) I just noticed a little typo in `ClosedState::STATE` that should be `closed` instead of `open` :) – Eugenio Carocci Jan 14 '21 at 08:55
0

What if you map state as a string and then:

public function setState(DoorState $state)
{
  $this->state = serialize($state);
}

and:

  private function state() 
  {
    return unserialize($this->state);
  }

  public function close()
  {
    $this->setState($this->state()->close())
  }
gvf
  • 1,039
  • 7
  • 6
  • Good idea but it kills a search like all Doors in a closed state. I'm going to spend a few more hours on it one day. Otherwise this it. – Clutch Dec 16 '15 at 22:31