Home » Php » php – How can I create a URL in a Console Controller in ZF2?

php – How can I create a URL in a Console Controller in ZF2?

Posted by: admin July 12, 2020 Leave a comment

Questions:

I have a Console Controller and an action to send emails (defined below in module.config.php)

'console' => array(
    'router' => array(
        'routes' => array(
            'cronroute' => array(
                'options' => array(
                    'route'    => 'sendEmails',
                    'defaults' => array(
                        'controller' => 'Application\Controller\Console',
                        'action' => 'send-emails'
                    )
                )
            ),              
        )
    )
),

In the action I want to send an email that contains a link to another action on the site. This would normally be done with a URL View Helper, but since the Request is of type Console and not HTTP, that doesn’t work. I’ve tried to create an HTTP request, but I do not know how to give it the site domain or the Controller/Action link.

My Controller code:

$vhm = $this->getServiceLocator()->get('viewhelpermanager');
$url = $vhm->get('url');
$urlString = $url('communication', array('action' => 'respond', 'id' => $id,
    array('force_canonical' => true));

This throws an error:

======================================================================
   The application has thrown an exception!
======================================================================
Zend\Mvc\Router\Exception\RuntimeException
Request URI has not been set

How do I create an HTTP Request in a Console Controller that has the site scheme, domain and path/to/action? And how do I pass it along to the URL View Helper?

How to&Answers:

Here’s how this issue can be solved:

<?php

// Module.php

use Zend\View\Helper\ServerUrl;
use Zend\View\Helper\Url as UrlHelper;
use Zend\Uri\Http as HttpUri;
use Zend\Console\Console;
use Zend\ModuleManager\Feature\ViewHelperProviderInterface;

class Module implements ViewHelperProviderInterface
{

    public function getViewHelperConfig()
    {
        return array(
            'factories' => array(
                'url' => function ($helperPluginManager) {
                    $serviceLocator = $helperPluginManager->getServiceLocator();
                    $config = $serviceLocator->get('Config');

                    $viewHelper =  new UrlHelper();

                    $routerName = Console::isConsole() ? 'HttpRouter' : 'Router';

                    /** @var \Zend\Mvc\Router\Http\TreeRouteStack $router */
                    $router = $serviceLocator->get($routerName);

                    if (Console::isConsole()) {
                        $requestUri = new HttpUri();
                        $requestUri->setHost($config['website']['host'])
                            ->setScheme($config['website']['scheme']);
                        $router->setRequestUri($requestUri);
                    }

                    $viewHelper->setRouter($router);

                    $match = $serviceLocator->get('application')
                        ->getMvcEvent()
                        ->getRouteMatch();

                    if ($match instanceof RouteMatch) {
                        $viewHelper->setRouteMatch($match);
                    }

                    return $viewHelper;
                },
                'serverUrl' => function ($helperPluginManager) {
                    $serviceLocator = $helperPluginManager->getServiceLocator();
                    $config = $serviceLocator->get('Config');

                    $serverUrlHelper = new ServerUrl();
                    if (Console::isConsole()) {
                        $serverUrlHelper->setHost($config['website']['host'])
                            ->setScheme($config['website']['scheme']);
                    }

                    return $serverUrlHelper;
                },
            ),
        );
    }
}

Of course you have to define default host and scheme values in config since there’s no way to detect them automatically in console mode.

Answer:

Update:
The correct answer for this post can be found here:
Stackoverflow: Using HTTP routes within ZF2 console application

Well you’re very close to this but you are not using the Url plugin. If you dived a little bit further into the ZF2 Documentation of the Controller Plugins you could have found the solution.

See for reference: ZF2 Documentation – Controller plugins

Your ConsoleController has to implement one of the following, to be able to retrieve the Controller plugins:

  1. AbstractActionController
  2. AbstractRestfulController
  3. setPluginManager

Well I recommend to extend your controller with the AbstractActionController if you haven’t done it yet.

In case you do use the AbstractActionController you can simply call $urlPlugin = $this->url() since the AbstractActionController has an __call() implementation retrieving the plugin for you. But you can also use: $urlPlugin = $this->plugin('url');

So in order to generate the URL for your mail you can do the following in your controller:

$urlPlugin = $this->url();
$urlString = $urlPlugin->fromRoute(
    'route/myroute',
    array(
        'param1' => $param1,
        'param2' => $param2
    ),
    array(
        'option1' => $option1,
        'option2' => $option2
    )
);

You can now pass this URL to your viewModel or use the URL viewHelper within your viewModel, but that is up to you.

Try to avoid viewHelpers within your controller as we’ve got plugins available for this case.

In case you wonder what methods the AbstractActionController has, here is ZF2 ApiDoc – AbstractActionController

In order to make this work you have to setup your route config with a proper structure:

// This can sit inside of modules/Application/config/module.config.php or any other module's config.
array(
    'router' => array(
        'routes' => array(
            // HTTP routes are here
        )
    ),

    'console' => array(
        'router' => array(
            'routes' => array(
                // Console routes go here
            )
        )
    ),
)

If you’ve got a Console module, just stick with the console route paths. Don’t forget the console key with all the routes beneath it! Take a look at the documentation for a reference: ZF2 – Documentation: Console routes and routing

Answer:

I can’t believe it but I did it 🙂

Hope it will work for all of You.

In controller where the fromRoute() function is used I added those lines below:

    $event  = $this->getEvent();
    $http   = $this->getServiceLocator()->get('HttpRouter');
    $router = $event->setRouter($http);
    $request = new \Zend\Http\Request();
    $request->setUri('');
    $router = $event->getRouter();
    $routeMatch = $router->match($request);

    var_dump($this->url()->fromRoute(
                         'route_parent/route_child',
                         [
                            'param1' => 1,
                            'param2' => 2,
                         )
    );

Output:

//mydomain.local/route-url/1/2

Of course route_parent/route_child is not a console route but HTTP route 🙂

Answer:

Thank @Alexey Kosov for response. You will probably have an issue when your application is working under subdirectory rather than root directory after domain ‘/’.

You have to add:

$router->setBaseUrl($config['website']['path']);

Whole code:

<?php

// Module.php

use Zend\View\Helper\ServerUrl;
use Zend\View\Helper\Url as UrlHelper;
use Zend\Uri\Http as HttpUri;
use Zend\Console\Console;
use Zend\ModuleManager\Feature\ViewHelperProviderInterface;

class Module implements ViewHelperProviderInterface
{

    public function getViewHelperConfig()
    {
        return array(
            'factories' => array(
                'url' => function ($helperPluginManager) {
                    $serviceLocator = $helperPluginManager->getServiceLocator();
                    $config = $serviceLocator->get('Config');

                    $viewHelper =  new UrlHelper();

                    $routerName = Console::isConsole() ? 'HttpRouter' : 'Router';

                    /** @var \Zend\Mvc\Router\Http\TreeRouteStack $router */
                    $router = $serviceLocator->get($routerName);

                    if (Console::isConsole()) {
                        $requestUri = new HttpUri();
                        $requestUri->setHost($config['website']['host'])
                            ->setScheme($config['website']['scheme']);
                        $router->setRequestUri($requestUri);
                        $router->setBaseUrl($config['website']['path']);
                    }

                    $viewHelper->setRouter($router);

                    $match = $serviceLocator->get('application')
                        ->getMvcEvent()
                        ->getRouteMatch();

                    if ($match instanceof RouteMatch) {
                        $viewHelper->setRouteMatch($match);
                    }

                    return $viewHelper;
                },
                'serverUrl' => function ($helperPluginManager) {
                    $serviceLocator = $helperPluginManager->getServiceLocator();
                    $config = $serviceLocator->get('Config');

                    $serverUrlHelper = new ServerUrl();
                    if (Console::isConsole()) {
                        $serverUrlHelper->setHost($config['website']['host'])
                            ->setScheme($config['website']['scheme']);
                    }

                    return $serverUrlHelper;
                },
            ),
        );
    }
}

Answer:

I had an similar problem with zend console – the serverUrl view helper also not worked properly by default.


My case:

/module/Application/src/Application/Controller/ConsoleController.php

...
public function someAction()
{
    ...
    $view = new ViewModel([
        'data' => $data,
    ]);
    $view->setTemplate('Application/view/application/emails/some_email_template');
    $this->mailerZF2()->send(array(
        'to' => $data['customer_email'],
        'subject' => 'Some email subject',
    ), $view);
    ...
}

/module/Application/view/application/emails/some_email_template.phtml

<?php
/** @var \Zend\View\Renderer\PhpRenderer $this */
/** @var array $data */
?><!doctype html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link ... rel="stylesheet" />
    <title>...</title>
</head>
<body>
<div style="...">
    ... <a href="<?= $this->serverUrl() ?>"><img src="<?= $this->serverUrl() ?>/images/logo-maillist.png" width="228" height="65"></a> ...
    <p>Hello, <?= $this->escapeHtml($data['customer_name']) ?>!</p>
    <p>... email body ...</p>
    <div style="...">
        <a href="<?= $this->serverUrl() ?>/somepath/<?= $data['some-key'] ?>" style="...">some action</a> ...
    </div>
    ...
</div>
</body>
</html>

The serverUrl view helper returns just only "http://" under Console Controller (runned by cron). But the same template renders properly under web http requests handled by other controllers.


I fixed it by this way:

/config/autoload/global.php

return array(
    ...
    'website' => [
        'host' => isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'my.production.domain',
        'scheme' => 'https',
        'path' => '',
    ],
);

/config/autoload/local.php

return array(
    ...
    'website' => [
        'host' => isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'my.local.domain',
    ],
);

/public/index.php (the ZF2 engine starting script)

chdir(dirname(__DIR__));

// --- Here is my additional code ---------------------------
if (empty($_SERVER['HTTP_HOST'])) {
    function array_merge_recursive_distinct(array &$array1, array &$array2)
    {
        $merged = $array1;
        foreach ($array2 as $key => &$value) {
            if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
                $merged[$key] = array_merge_recursive_distinct($merged[$key], $value);
            } else {
                $merged[$key] = $value;
            }
        }
        return $merged;
    }

    $basicConfig = require 'config/autoload/global.php';
    $localConfig = @include 'config/autoload/local.php';
    if (!empty($localConfig)) {
        $basicConfig = array_merge_recursive_distinct($basicConfig, $localConfig);
    }
    unset($localConfig);

    $_SERVER['HTTP_HOST'] = $basicConfig['website']['host'];
    $_SERVER['HTTP_SCHEME'] = $basicConfig['website']['scheme'];
    $_SERVER['HTTPS'] = $_SERVER['HTTP_SCHEME'] === 'https' ? 'on' : '';
    $_SERVER['SERVER_NAME'] = $_SERVER['HTTP_HOST'];

    unset($basicConfig);
}
// ---/End of my additional code ---------------------------

// Setup autoloading
require 'init_autoloader.php';
...

And that’s all that I changed.

Magic! It works! 🙂

Hope this helps to someone too.

Answer:

I think the best solution is using a DelegatorFactory.

config/autoload/server-url.local.php:

return [
    'server_url' => 'http://example.com',
];

module/Application/config/module.config.php:

'service_manager' => [
    'delegators' => [
        TreeRouteStack::class => [
            TreeRouteStackConsoleDelegatorFactory::class,
        ],
    ]
],

module/Application/src/TreeRouteStackConsoleDelegatorFactory.php:

namespace Application;

use Interop\Container\ContainerInterface;
use Zend\Router\Http\TreeRouteStack;
use Zend\ServiceManager\Factory\DelegatorFactoryInterface;
use Zend\Uri\Http;

class TreeRouteStackConsoleDelegatorFactory implements DelegatorFactoryInterface
{
    public function __invoke(ContainerInterface $container, $name, callable $callback, array $options = null)
    {
        /** @var TreeRouteStack $treeRouteStack */
        $treeRouteStack = $callback();

        if (!$treeRouteStack->getRequestUri()) {
            $treeRouteStack->setRequestUri(
                new Http($container->get('config')['server_url'])
            );
        }

        return $treeRouteStack;
    }
}