Search code examples
phpabstract-factory

Allowing custom class instantiation with the abstract factory pattern in PHP


Currently, I use an abstract factory to allow the specification of a custom class name for generating a request object. My reasoning for doing this is to allow me to easily extend core functionality without altering code. Recently, though, I've had some doubts as to the efficacy of this approach. So my question is this:

Is allowing the factory to instantiate any submitted class name that matches the expected interface a bastardization of the factory concept? Would I be better served to avoid this?

UPDATE

The logic here is this: on the one hand, a real-life car factory (for example) can't create a car if it's not equipped with the machinery to make that kind of car. On the other hand, the code below is like giving that same car factory the blueprint to make the custom car it wasn't originally intended to build.

An alternative would be to pass in a configuration object specifying a custom class name that may be used with the factory and limit the factory to producing a custom class only if it specifically matches the config-specified custom class name. Any thoughts?


And the relevant code ...

<?php

interface AbstractRequestFactory
{
  public function buildRequest($type);
}

class RequestFactory implements AbstractRequestFactory
{
  public function buildRequest($type='http')
  {
    if ($type == 'http') {
      return new HttpRequest();
    } elseif ($type == 'cli') {
      return new CliRequest();
    } elseif ($custom = $this->makeCustom($type)){
      return $custom;
    } else {
      throw new Exception("Invalid request type: $type");
    }
  }

  protected function makeCustom($type)
  {
    if (class_exists($type, FALSE)) {
      $custom = new $type;
      return $custom instanceof RequestInterface ? $custom : FALSE;
    } else {
      return FALSE;
    }
  }
}

// so using the factory to create a custom request would look like this:

class SpecialRequest implements RequestInterface {}

$factory = new RequestFactory();
$request = $factory->buildRequest('\SpecialRequest');

Solution

  • What you have looks pretty good. The point of having a factory is to pass in some criteria, and have the method return you back an object, which you assume will have the same callable methods available to the calling code. You are enforcing this assumption by implementing the RequestInterface, so as long as any custom request classes implement the same interface, you won't end up in a 'unable to call function on non-object' scenario.

    A couple of recommendations (just personal preference):

    • I would use switch / case on $type in buildRequest

    • I would return null or object from makeCustom(), otherwise you are mixing return types (object and bool)

    • Depending on how many custom types you have, I would actually hard code them into the switch case, just to alleviate any confusion. Don't get me wrong, what you have is great if you have a lot of classes, but chances are you don't.

    • Did you ever consider putting the "easily extend core functionality without altering code" piece into an abstract parent class, which can be extended by custom type classes?

    • Also, because a factory creates objects, it's common practice to set it as static.

    Example code snippet:

    public static function getRequest($type='http')
    {
        switch ($type) {
    
            case 'http':
                return new HttpRequest();
    
            case 'cli':
                return new CliRequest();
    
            case 'myCustom1':
                return new MyCustom1();
    
            case 'myCustom2':
                return new MyCustom2();
    
            default: 
                throw new Exception("Invalid request type: $type");
        }
    }
    
    $request = RequestFactory::getRequest($type);
    
    // As long as all objects in factory have access to same methods
    $request->doSomething();
    $request->andDoSomethingElse();
    
    // Otherwise you end up with that feared 'unable to call function on non-object'
    $request->iAmASneakyMethodNotEnforcedByAnInterfaceOrAvailableByExtension();