2
\$\begingroup\$

Aloha,

I am working to understand some of the different pieces of domain driven design. In this particular case, I have a NameFactory that builds a Name domain object. I also have a NamePresenter that prepares data for output.

The NameFactory creates a Name, formatting it based on ways that the data may have been input. It can receive either 1-3 words, or an array of properties (from a database call). We can optionally include a preferred name, or alias, for a person. This is entered in either the first or middle name field, and put in parentheses or quotes.

The NamePresenter is used for output formatting, in several different styles. The "Internal" style includes a span tag. I truly have no idea if that's reasonable coding style in a presenter, or something that needs to be delegated to a view.

The Name class itself might be a bad example, because it doesn't actually have any behavior of its own. It just uses a couple of traits to load data via the constructor and expose private properties for read-only access. I think that's the only way to make an Immutable object in php?

I started with just a Name object which handled both input handling and output formatting. It's gone through a couple of phases of refinement to get here. It seems more complex, now, but I think it's a better design?

I would appreciate any input on this. It's new to me, and I'd like to really learn how to use these patterns properly.

Thank you!

Factory:

<?php namespace App\Person;

use App\Exceptions\InvalidAttributeException;
use App\Exceptions\MissingAttributeException;


class NameFactory
{
    private $args;
    private $preferred;

    public static function make(...$args)
    {
        // TODO: Make a NullName class?
        if (count($args) === 0)
            throw new MissingAttributeException('You must provide a name');

        if (count($args) > 3)
            throw new InvalidAttributeException('Too many parameters were passed');

        $builder = new self;
        return new Name($builder->getParams($args));
    }

    private function getParams(array $args)
    {
        $this->args = is_array($args[0]) ? $args[0] : $args;
        $this->extractPreferred();

        return [
            'preferred' => $this->getPreferred(),
            'first' => $this->getFirst(),
            'middle' => $this->getMiddle(),
            'last' => $this->getLast(),
            'prefix' => $this->getArg('prefix'),
            'suffix' => $this->getArg('suffix'),
        ];
    }

    private function getArg($name)
    {
        if (isset($this->args[$name]))
            return trim($this->args[$name]);

        return '';
    }

    private function getPreferred()
    {
        if ($this->preferred)
            return $this->preferred;

        if ($arg = $this->getArg('preferred'))
            return $arg;

        if (count($this->args) === 1)
            return $this->getFirst();

        return '';
    }

    private function getFirst()
    {
        return $this->getArg('first')
            ?: trim($this->args[0]);
    }

    private function getMiddle()
    {
        if ($arg = $this->getArg('middle'))
            return $arg;

        if (count($this->args) === 3)
            return trim($this->args[1]);

        return '';
    }

    private function getLast()
    {
        if ($arg = $this->getArg('last'))
            return $arg;

        if (count($this->args) > 1)
            return trim($this->args[count($this->args) - 1]);

        return '';
    }

    private function extractPreferred()
    {
        // TODO: Any way to not repeat regular expressions?
        foreach($this->args as &$arg) {
            $matches = [];
            if (preg_match('/\((.*)\)/m', $arg, $matches)) {
                if (count($matches) >= 1) {
                    // use first value only
                    $this->preferred = $this->preferred ?: $matches[1];
                    // remove everything in parentheses
                    $arg = str_replace("($matches[1])", '', $arg);
                }
            }
            if (preg_match('/"(.*)"/m', $arg, $matches)) {
                if (count($matches) >= 1) {
                    // use first value only
                    $this->preferred = $this->preferred ?: $matches[1];
                    // remove everything in quotes
                    $arg = str_replace("\"$matches[1]\"", '', $arg);
                }
            }
        }
    }

}

Domain Object:

<?php namespace App\Person;

use App\Traits\LoadsAttributesFromArray;
use App\Traits\ExposesGetters;


class Name
{
    use LoadsAttributesFromArray,
        ExposesGetters;

    private $first;
    private $last;
    private $middle;
    private $preferred;
    private $prefix;
    private $suffix;
}

LoadsAttributesFromArray Trait:

<?php namespace App\Traits;

use App\Exceptions\InvalidAttributeException;


trait LoadsAttributesFromArray
{

    public function __construct(array $attrs)
    {
        $this->setAttributes($attrs);
    }

    private function setAttributes($attrs)
    {
        foreach($attrs as $key => $value)
            $this->setAttribute($key, $value);
    }

    private function setAttribute($key, $value)
    {
        if (property_exists($this, $key))
            return $this->$key = $value;

        throw new InvalidAttributeException("Unknown attribute $key");
    }

}

ExposesGetters trait:

<?php namespace App\Traits;

use App\Exceptions\InvalidAttributeException;


trait ExposesGetters
{
    public function __get($key)
    {
        if (property_exists($this, $key))
            return $this->$key;

        throw new InvalidAttributeException("Unknown attribute $key");
    }
}

Presenter:

<?php namespace App\Person;

use Illuminate\Support\Str;


class NamePresenter
{
    private $name;

    function __construct(Name $name)
    {
        $this->name = $name;
    }

    public function __get($key)
    {
        $method = 'get' . Str::studly($key);
        if (method_exists($this, $method))
            return $this->$method();

        return $this->name->$key;
    }

    public function getFirstName()
    {
        return $this->name->first;
    }

    public function getLastName()
    {
        return $this->name->last;
    }

    /**
     * Get a name for displays. This is either the
     * preferred name (if available) or the first name
     */
    public function getDisplay()
    {
        $preferred = $this->name->preferred;
        if ($preferred)
            return $preferred;

        return $this->name->first;
    }

    public function getLegalName()
    {
        $name = '';
        foreach(['first','middle','last'] as $el) {
            $name = trim($name . ' ' . $this->name->$el);
        }
        return $name;
    }

    /**
     * Get the full name in the format:
     *
     *    Prefix First Middle Last Suffix
     */
    public function getFullName()
    {
        $name = "";
        foreach(['prefix','first','middle','last','suffix'] as $el) {
            $name = trim($name . ' ' . $this->name->$el);
        }
        return $name;
    }

    /**
     * Show the internal name; a combination of the
     * display name and the legal name.
     */
    public function getInternal()
    {
        $display = $this->getDisplay();

        if ($legal = $this->getFullName())
            if ($legal !== $display)
                return "$display <span class='full-name'>$legal</span>";

        return $display;
    }

    public function getInternalName()
    {
        return $this->getInternal();
    }

    public function __toString()
    {
        return $this->getInternal();
    }

}
\$\endgroup\$
1
\$\begingroup\$

I have adjusted the NameFactory class to work correctly with nulls, and to be a bit more concise. The new class is as follows:

<?php namespace App\Person;

use App\Exceptions\InvalidAttributeException;
use App\Exceptions\MissingAttributeException;


class NameFactory
{
    private $argc;
    private $args;
    private $named;
    private $preferred;

    public static function make(...$args)
    {
        // TODO: Make a NullName class?
        if (count($args) === 0)
            throw new MissingAttributeException('You must provide a name');

        if (count($args) > 3)
            throw new InvalidAttributeException('Too many parameters were passed');

        $builder = new self;
        return new Name($builder->getParams($args));
    }

    private function getParams(array $args)
    {
        $this->argc = count($args);
        $this->named = is_array($args[0]);
        $this->args = $this->named ? $args[0] : $args;
        $this->extractPreferred();

        return [
            'preferred' => $this->getPreferred(),
            'first' => $this->getFirst(),
            'middle' => $this->getMiddle(),
            'last' => $this->getLast(),
            'prefix' => $this->getArg('prefix'),
            'suffix' => $this->getArg('suffix'),
        ];
    }

    private function getArg($key)
    {
        if (isset($this->args[$key]))
            return trim($this->args[$key]);

        return '';
    }

    private function getPreferred()
    {
        if ($this->preferred)
            return $this->preferred;

        return ($this->argc === 1)
            ? $this->getFirst()
            : '';
    }

    private function getFirst()
    {
        return $this->getArg('first')
            ?: $this->getArg('0');
    }

    private function getMiddle()
    {
        if ($this->named)
            return $this->getArg('middle');

        return ($this->argc === 3)
            ? $this->getArg('1')
            : '';
    }

    private function getLast()
    {
        if ($this->named)
            return $this->getArg('last');

        return $this->getArg($this->argc - 1);
    }

    private function extractPreferred()
    {
        if ($preferred = $this->getArg('preferred'))
            $this->preferred = $preferred;

        foreach($this->args as &$arg) {
            $this->extractPreferredExpression(
                '/\((.*)\)/m','(',')', $arg
            );
            $this->extractPreferredExpression(
                '/"(.*)"/m', "\"","\"", $arg
            );
        }
    }

    private function extractPreferredExpression(
        $regex, $left, $right, &$arg
    ) {
        $matches = [];
        if (preg_match($regex, $arg, $matches)) {
            if (count($matches) >= 1) {
                // use first value only
                $this->preferred = $this->preferred ?: $matches[1];
                // remove everything in parentheses
                $arg = str_replace("{$left}{$matches[1]}{$right}", '', $arg);
            }
        }
    }

}

To create a name, I use NameFactory::make('foo','a','bar'), or NameFactory::make(['first'=>'foo','last'=>'bar']). To prepare the data for output, I am using (new NamePresenter($name))->output_format.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.