Solution 3: Mock The Clock


Whilst using a Doctrine Listener works, to me, it feels wrong.

The concept of keeping track of the creation date of a given object is about the limit of what I am happy with happening automatically.

With Widget having these createdAt and updatedAt properties managed automatically for me does feel like I'm saving myself time. These properties feel like something of which I should be keeping track.

But what are they giving me, really?

Ok, so createdAt seems useful enough.

There is (or should be) only one time this property ever gets set: at object instantiation.

updatedAt though is a lot more hazy.

What we're really saying is lastUpdatedAt... and that's about it.

We don't capture by whom, for what reason, and exactly what changed.

We could, and in some cases we should be interesting in all three of these data points. But for the purposes of this short tutorial I am not going to go down that rabbit hole.

Right now all we will achieve is the setting of createdAt, and updatedAt, and preferably without using a Doctrine Listener.

Solution 3

We can remove the Doctrine Listener from the previous solution.

We can remove the HasLifecycleCallbacks, and the PrePersist and PreUpdate annotations on the setters.

Why?

Because we're going to take ownership of calling the setters.

We can use a Factory to encapsulate the creation of a Widget. As part of creating the Widget we can ensure the setCreatedAt method is called.

And because our Factory will be a Symfony service we can easily inject our Clock service into that Factory.

Here's the test spec:

<?php

namespace spec\AppBundle\Factory;

use AppBundle\Factory\WidgetFactory;
use AppBundle\Model\Clock;
use PhpSpec\ObjectBehavior;

class WidgetFactorySpec extends ObjectBehavior
{
    /**
     * @var Clock
     */
    private $clock;

    function let(Clock $clock)
    {
        $this->clock = $clock;

        $this->beConstructedWith($clock);
    }

    function it_is_initializable()
    {
        $this->shouldHaveType(WidgetFactory::class);
    }

    function it_can_create_a_widget()
    {
        $now = new \DateTimeImmutable('now');

        $this->clock->now()->willReturn($now);

        $widget = $this->create();

        $widget->getCreatedAt()->shouldEqual($now);
    }
}

and the associated code:

<?php

namespace AppBundle\Factory;

use AppBundle\Entity\Widget;
use AppBundle\Model\Clock;

class WidgetFactory
{
    /**
     * @var Clock
     */
    private $clock;

    public function __construct(
        Clock $clock
    )
    {
        $this->clock = $clock;
    }

    public function create()
    {
        return (new Widget())
            ->setCreatedAt(
                $this->clock->now()
            )
        ;
    }
}

And the service definition:

services:
    crv.factory.widget:
        class: AppBundle\Factory\WidgetFactory
        arguments:
            - "@crv.clock"

That's ok.

We might want to think about a guard clause inside the setCreatedAt method to ensure it cannot be called if the class property it sets (private $createdAt) is not null.

A JSON API controller action to handle POST requests might then look like this:

    /**
     * Create new Widget from the submitted data
     *
     * @Annotations\Post(path="/widget")
     */
    public function postAction(Request $request)
    {
        // creates a Widget with the `createdAt` property already set
        $widget = $this->get('crv.factory.widget')->create();

        $form = $this->createForm(WidgetType::class, $widget, [
            'csrf_protection' => false,
        ]);

        $form->submit($request->request->all(), false);

        if (!$form->isValid()) {
            return $form;
        }

        $widget = $form->getData();

        $em = $this->getDoctrine()->getManager();
        $em->flush();

        $routeOptions = [
            'id'      => $widget->getId(),
            '_format' => $request->get('_format'),
        ];

        return $this->routeRedirectView('get_widget', $routeOptions, Response::HTTP_NO_CONTENT);
    }

What about setting updatedAt though?

Well, we have numerous options.

Simply enough we can just do it ourselves before calling $this->em->flush():

    /**
     * Update existing Widget from the submitted data
     *
     * @Annotations\Put(path="/widget/{id}")
     */
    public function putAction(Request $request, int $id)
    {
        // getRepo() being a private method to return
        // whatever repo we have configured
        $widget = $this->getRepo()->find($id);

        $form = $this->createForm(WidgetType::class, $widget, [
            'csrf_protection' => false,
        ]);

        $form->submit($request->request->all(), false);

        if (!$form->isValid()) {
            return $form;
        }

        $widget = $form->getData();

        // a manual process
        $widget->setUpdatedAt();

        $em = $this->getDoctrine()->getManager();
        $em->flush();

        $routeOptions = [
            'id'      => $widget->getId(),
            '_format' => $request->get('_format'),
        ];

        return $this->routeRedirectView('get_widget', $routeOptions, Response::HTTP_NO_CONTENT);
    }

We could get fancier than this.

We could also - potentially - opt to keep an array of update history, rather than just blindly call setUpdatedAt.

The options here are open to us, and this is a good thing.

In this approach we aren't tied to Doctrine. A listener won't get called for every entity we create.

All this said, it sure is a lot of work to enable us to write a test that checks a bit of JSON :)

Do you have an alternative approach? I'd love to hear it.

Code For This Course

Get the code for this course.

Episodes

# Title Duration
1 Solution 1: Approximation 11:04
2 Solution 2: (ab)Using Doctrine 15:44
3 Solution 3: Mock The Clock 10:02