Question

I'm developing a WebApp which (as usual) must support customer specific functionalities. To achieve it I plan to set the customer name in the local app configuration (config/autoload/local.php ) configuration file so that I can use it to call the specialized code later on. The module folder structure is this:

/module/Application
/module/Application/config
/module/Application/src
/module/Application/src/Application
/module/Application/src/Application/Controller
/module/Application/src/Application/Controller/[customer_instance_name]
/module/Application/src/Application/Model
/module/Application/src/Application/Model/[customer_instance_name]
/module/Application/view
/module/Application/view/Application
/module/Application/view/Application/[action]
/module/Application/view/Application/[action]/[customer_instance_name]

Using a custom ViewModel I inject the specific customer name to the template path:

namespace Application\Model;
use Zend\View\Model\ViewModel;
use Zend\View\Resolver\TemplatePathStack;
use Zend\Mvc\Service\ViewTemplatePathStackFactory;
class MyViewModel extends ViewModel
{
    private $customInstanceName;
    private $pathStack;

    /**
     * Constructor
     *
     * @param  null|array|Traversable $variables
     * @param  array|Traversable $options
     */
    public function __construct($variables = null, $options = null)
    {
        parent::__construct ( $variables, $options );
        $serviceLocator = MySingleton::instance()->serviceLocator;
        $factory = new ViewTemplatePathStackFactory();
        $this->pathStack = $factory->createService($serviceLocator);

        $config = $serviceLocator->get('config');
        if (isset($config['custom_instance_name']) AND ($config['custom_instance_name']!='')) {
            $this->customInstanceName = $config['custom_instance_name']; 
        } else {
            $this->customInstanceName = false;
        }
    }

    /**
     * Set the template to be used by this model
     *
     * @param  string $template
     * @return ViewModel
     */
    public function setTemplate($template)
    {
        $this->template = (string) $template;
        if ( $this->customInstanceName === false) {
            return $this;
        }
        $pathComponents = explode('/', (string) $template);
        $last = array_pop($pathComponents);
        array_push($pathComponents, $this->customInstanceName);
        array_push($pathComponents, $last);
        $customTemplate = implode('/', $pathComponents);
        if ($this->pathStack->resolve($customTemplate) !== false) {
            $this->template = $customTemplate;
        }
        return $this;
    }
}

Using the "Decorator Pattern" I can achieve the same customization level on my Models.

I'm having problem to handle specific behavior. In this case I plan to create custom Controllers extending my base controller class, but I'unable to call those controllers since the routing is defined on the module config (and I was unable to change it in runtime).

My questions are:

1) Is this approach correct, or there is a better way to do it?

2) If the approach is correct, how can I define a custom router to be used when the ServiceManager reads my routing config?

Was it helpful?

Solution

Just found a solution. Will register it here hoping someone will benefit from it.

All I had to do was to create a specific router class with a match method which returns the correct routing target for each customer controller, and add it to my module.config.php as the type for each action.

namespace TARGETNAMESPACE;
use Traversable;
use Zend\Mvc\Router\Exception;
use Zend\Mvc\Router\Http\RouteInterface;
use Zend\Mvc\Router\Http\RouteMatch;
use Zend\Mvc\Router\Http\Literal;
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\RequestInterface as Request;

class MyRouterLiteral extends Literal {

    public function match(Request $request, $pathOffset = null) {
        if (! method_exists($request, 'getUri')) {
            return null;
        }

        $uri = $request->getUri();
        $path = $uri->getPath();

        if ($pathOffset !== null) {
            if ($pathOffset >= 0 && strlen($path) >= $pathOffset && ! empty($this->route)) {
                if (strpos($path, $this->route, $pathOffset) === $pathOffset) {
                    return new RouteMatch($this->getDefaults(), strlen($this->route));
                }
            }

            return null;
        }

        if ($path === $this->route) {
            return new RouteMatch($this->getDefaults(), strlen($this->route));
        }

        return null;
    }

    private function getDefaults() {
        $aux = explode('\\', $this->defaults['controller']);
        $last = array_pop($aux);
        array_push($aux, '[CUSTOM_INSTANCE_NAME]');
        array_push($aux, '[CUSTOM_INSTANCE_NAME]'.$last);
        $result = $this->defaults;
        $result['controller'] = implode('\\', $aux);
        return $result;
    }
}

To address all cases I had to create a second custom router (for segment routes) which follows the same rules and can be easily derived from the above code.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top