Skip to content

Instantly share code, notes, and snippets.

@jtallant
Created December 8, 2015 18:51
Show Gist options
  • Save jtallant/075201a2e529e5d72251 to your computer and use it in GitHub Desktop.
Save jtallant/075201a2e529e5d72251 to your computer and use it in GitHub Desktop.

Screenshots

Feature: Send a screenshot when an issue is submitted.

Analyzing the feature:

A screenshot of an issue helps the developer/pm recognize the issue. The screenshot could help isolate the problem which will help ensure the problem is correctly recognized and fixed. The screenshot could also help the developer fix the bug faster and reduce communication time.

The requested feature says to just send one image with the issue. The image is a screenshot of whatever page the user was on when they submitted the issue.

Even though the feature only asks for one screenshot. It's very possible that users will eventually want to attach multiple images to an issue. These images could be attached in different locations with different methods. Maybe users will want to attach more images to an issue through the dispatch web UI. Maybe users will eventually want to attach multiple images through the embedded widget.

Now we need to decide if we should code the feature with the ability to have multiple images attached to an issue from the beginning, or code it to only have one and change it later if we need to.

To make this decision we can weigh the expected dev time of attaching multiple images against just attaching one. We should also consider the dev time of making the change later. Keeping in mind, the change isn't guaranteed.

After thinking about the implementation, I estimate that coding it to attach multiple images is almost as easy as coding it to attach one, and that changing this functionality to support multiple images later would be harder than just supporting multiple images right now.

Note that we will be coding the backend to support relating multiple images to an issue. We won't be implementing any type of image upload in the embedded widget or the web UI yet. That can wait until it's actually requested. We will just code the backend to support multiple images. That way we won't need a schema change later.

Implementation

Determining the Schema

We're going to need to store some new information in the DB. So first we need to decide what the schema should look like.

No schema change should be made lightly. If we get it wrong and need to make another schema change later, after production data has been stored in the incorrect schema, we'll have to write commands to handle migrating the data over to reflect the new schema, and that can be time consuming and risky (data loss).

When making a schema change you should always consider the "domain". How will you talk about these concepts when communicating to your team or clients? Your entities, services, and objects should reflect that language. You should not hear a term and then have to translate that term to some other class name inside your code. There are exceptions to this. For example a PM would probably say "send a screenshot over when someone submits an issue". We will call screenshots images because screenshot is too specific. At a later date, it's possible that images, which are not screenshots, will be attached to an issue to help describe it in some other way.

It's clear that these images belong to an Issue. So we will add a new property to the Issue entity called images. The Issue entity will have a one to many relationship with images.

We'll need an images table with a foreign key to an issue id. We'll create a new entity class called image with a table name of images. We'll use doctrine annotations to relate issues to images. See the doctrine documentation for a bidirectional one to many association.

Steps
  • Create entity Image
  • Map issues to images via Doctrine annotations
  • Create repository images (every entity needs a repo)
  • Register the repository in the DoctrineRepository provider. This allows us to retrieve the repository from the container. It also means we can type hint the repository as a class dependency and have it automatically resolved by the container.

Storing the image

We need to send the image data over in the request. After looking at the current request object for issues/store, we can see that the image should be a direct attribute of issue because it belongs to the issue and not any property of issue. issue.images vs issue.client.images. The image doesn't really have anything to do with the http client. An image could later be attached to an issue via the web UI so we don't really care what http client the issue came from, if any.

We know we'll eventually need to send this data along in the request and that means we need to modify the javascript to send the data over. We'll do that last. We'll code all of the backend and test it with the request we want. Then we'll implement that request in the javascript.

We're using a command bus pattern in the application. When someone submits a request to create an issue. We send a CreateIssue command to the application. The command is executed and a handler class does the work. After doing the work, the handler fires off an event notifying the rest of the application that an issue has been created. That means that any listeners that care about new issues being created, can then be fired and do their designated tasks (create basecamp todo, send email, etc).

Because the image belongs to an issue. Storing the image is part of creating an issue, therefore we do not need a new command for this. The business of storing the image will be handled inside the CreateIssueHandler. We'll need to modify the CreateIssue command so it will hold whatever data the handler will need to store the image.

Storing an image is not a simple task. In this case we need to convert a dataUri to an image. Each class should have a single responsibility. The handler doesn't need to know how to convert a dataUri to an image. It just needs to have access to something that does. That means we need a class to handle converting a dataUri to an image. This is a service class that will be registered in the container. We will then inject this class as a dependency to the CreateIssueHandler.

But we should introduce another level of abstraction. Perhaps we need to attach an image to an issue that is an actual image and not a dataUri. We can code this in a way that allows us to support multiple methods of attaching an image to an issue. In one case, we might have an array of uploaded files that get sent to the CreateIssueHandler, in another case we may have an array of dataUris that need converted to images that get sent to the handler.

It sounds like we're going to need a transformer. Something that takes some data and transforms it into an Image object. Because we don't know what type of image data the handler will receive, the handler should depend on an interface that transforms data into an image. We can then create multiple implementations of this interface and bind whatever implementation in that we want at runtime.

<?php

# QA\Contracts
interface ImageTransformer
{
    public function transform($data);
}

# QA\Transformers
class DataUriToImage implements ImageTransformer
{
    public function transform($data)
    {
        # Code to convert dataUri to Image entity here.
        # return Image
    }
}

# We won't create this class yet, but it's an example
# of another implementation we may need in the future.
# QA\Transformers
class SplFileInfoToImage implements ImageTransformer
{
    public function transform($data)
    {
        # Code to convert an instance of SplFileInfo
        # to an Image entity here.
        # return Image
    }
}

# Register the provider
# in app/Providers
# Note: We also need to add the provider to config/app.php
# QA\Providers
class ImageTransformer extends ServiceProvider
{
    public function register()
    {
        # When someone type hints ImageTransformer
        # give them the DataUriToImageTransformer
        # Later we can easily swap this to another transformer
        # Read laravel docs on the service container and service providers.
        $this->app->bind('QA\Contracts\ImageTransformer', function($app) {
            return new DataUriToImageTransformer($someDependency, $someOtherDependency);
        });
    }
}

# CreateIssueHandler depends on a contract (interface).
# Not a concrete implementation.
# QA\Commands
class CreateIssueHandler
{
    protected $transformer;

    public function __construct(Transformer $transformer)
    {
        # Will receive the bound ImageToDataUri transformer
        $this->transformer = $transformer;
    }

    # This is roughly how the image stuff will be
    # handled in the handler. Note that there will be other code
    # in this method. This is just the image stuff.
    public function handle(Command $command)
    {
        $issue = new Issue;
        foreach($command->getImages() as $imageData) {
            $image = $this->transformer->transform($imageData);
            $issue->addImage($image);
        }

        $this->em->persist($issue);
        $this->em->flush();
    }
}

Now all that is left is to attach the transformed Image entities to the issue and persist the issue.

Then we can send those images to basecamp or wherever.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment