-2

I'm new to Symfony and am using 5.x. I have created a Console command using Symfony\Component\Console\Command\Command and am trying to use Symfony\Component\HttpClient\HttpClient to POST to a URL. I need to generate the URL to a route running on the same machine (but in future this may possibly change to a different machine), so the host could be like localhost or example.com, and the port of the API is custom. I have searched on the web but the only possible solution I got involved the use of Symfony\Component\Routing\Generator\UrlGeneratorInterface, and the web is cluttered with code samples for old versions of Symfony, and I haven't yet managed to get this working.

My latest attempt was:

public function __construct(UrlGeneratorInterface $router)
{
    parent::__construct();
    $this->router = $router;
}

but I don't really understand how to inject the parameter UrlGeneratorInterface $router to the constructor. I get an error that the parameter was not supplied. Do I have to create an instance of UrlGenerator elsewhere and inject it over here, or is there a simpler way to just generate an absolute URL in Symfony from within a Command? I don't really understand containers yet.

$url = $context->generate('view', ['Param' => $message['Param']], UrlGeneratorInterface::ABSOLUTE_URL);

services.yaml:

App\Command\MyCommand:
    arguments: ['@router.default']
  1. Is there a simpler way to generate a URL from a Console Command by explicitly specifying host, protocol, port, route, parameters etc?
  2. Why isn't UrlGeneratorInterface or RouterInterface autowiring?
  3. Do I need to specify wiring manually as $router.default in services.yaml if I also have autowiring enabled?
  4. I understand that the execute function implementation may be incorrect, but I couldn't get to fixing that without first getting the constructor working. This is still, work in progress.

EDIT: Updated gist: https://gist.github.com/tSixTM/86a29ee75dbd117c8f8571d458ed72db

EDIT 2: Made the problem statement clearer by adding question points: I slept on it :)

EDIT 3:

#!/usr/bin/env php
<?php
// application.php

require __DIR__.'/vendor/autoload.php';

use Symfony\Component\Console\Application;

$application = new Application();

$application->add(new App\Command\MyCommand());

$application->run();
Thejaka Maldeniya
  • 1,076
  • 1
  • 10
  • 20
  • It seems you don't understand Symfony very well yet. Seeing as the service container is a fundimental part of Symfony I would recommend tinkering around with the Symfony "Getting Started" tutorials. – Matt Smeets Jun 09 '20 at 16:52
  • @MattSmeets, I HAVE taken a preliminary look at the docs including Service Container, and I ran php bin/console debug:autowiring and router.default is shown which I tried before in services.yaml, but I think my confusion stems from the fact that I'm currently using a single project for both API and Console Command. These may be split into separate projects in the future. I don't understand if a container is shared between Console Commands and Web or not. I'm not even sure if these are covered in Getting Started. Or perhaps I don't understand services.yaml very well? – Thejaka Maldeniya Jun 09 '20 at 18:08
  • 2
    Ah I think I understand it more now. Every command that is run through `bin/console` goes through the same kernel as the "web-facing" side of your application. So functionally this is the same service container. Try accepting `RouterInterface` instead of `UrlGeneratorInterface`, reason for this is because you are passing `@router` It is common to create a single project for console and web, reasons to keep them together are shared logic, entities, and configuration. Another reason for this error could be that you have autowiring enabled. Should be under _defaults in services.yaml 1/2 – Matt Smeets Jun 09 '20 at 18:16
  • 1
    Thanks for the response @MattSmeets. I tried RouterInterface and still get same error: Uncaught ArgumentCountError: Too few arguments to function App\Command\MyCommand::__construct(), 0 passed... I DO have autowiring enabled but I was hoping I could leave it enabled. I tried explicitly providing autowire: false to MyCommand configuration in services.yaml, but no change – Thejaka Maldeniya Jun 09 '20 at 18:31
  • 1
    You can find the manual for the current version [here](https://symfony.com/doc/current/routing.html#generating-urls-in-commands). You'll need to configure the request context another way if you are using < 5.1 – msg Jun 09 '20 at 18:38
  • 1
    Thanks for your response @msg. Symfony 5.1.0 (env: dev, debug: true). I already tried that. It's basically the same as suggested by Matt Smeets ? I tried again just now to make sure, and get the same error. (I removed the explicit wiring in services.yaml, hoping it would autowire) I didn't configure routing.yaml, but that shouldn't be a problem right? It means I should get localhost? I just don't understand why it isn't autowiring... I'm using Windows 10 btw. – Thejaka Maldeniya Jun 09 '20 at 18:57
  • auto wiring takes priority over custom service definitions. I wouldn't mind helping you debug if you can provide a gist of relevant code – Matt Smeets Jun 09 '20 at 19:01
  • 1
    Yes, you should get `http://localhost/` but if you want to change it to `example.com` as you say in your question you need to configure it explicitly. symfony commands know nothing about your vhost since it's running outside the server context. – msg Jun 09 '20 at 19:03

3 Answers3

2

I tinkered around with your gist and found the following to work: https://gist.github.com/Matts/528c249a82e5844164039c4f6c0db046

The problem that you seemed to have, was not due to your service declaration, rather it was that you were missing the declaration of the private $router variable in MyCommand, see line 25.

So you can keep the services.yaml as you show in your gist, no changes required to the autowire variable, also you don't have to manually declare the command

Further, you don't need to fetch $context from the router, you can also set the base URL in your framework.yaml, here you can find where I found this.

Please note that I removed some code from the execute, this was due to me not having access to your other files. You can just re-add this.

Community
  • 1
  • 1
Matt Smeets
  • 398
  • 4
  • 15
  • 1
    Sorry, when I try it, it fails before reaching the assignment to private $router. I guess that's why I missed that. The error I still get is Uncaught ArgumentCountError: Too few arguments to function App\Command\MyCommand::__construct(), 0 passed... meaning the constructor doesn't get called. When I run php bin/console debug:autowiring, Symfony\Component\Routing\RouterInterface (router.default) is listed. Could I be missing a required package? I started with symfony/skeleton, and added packages as required. FYI I'm using PHP 7.4.6 on Windows 10 x64 2004. – Thejaka Maldeniya Jun 10 '20 at 13:27
  • Is your services.yaml controller exactly the same as here: https://gist.github.com/Matts/b053809ec536a1b0e105edcd1124d822 ? – Matt Smeets Jun 13 '20 at 16:49
  • Sorry for not responding sooner... I was a bit busy with other things, but I think I figured out the problem. I think it's this line: $application->add(new App\Command\MyCommand()); in application.php. I use that to invoke the command. Have you an idea how to fix that? I'll share the code in an edit... – Thejaka Maldeniya Jun 15 '20 at 16:22
  • Maybe you'll find this useful https://dev.to/yannpewpew/symfony-console-crash-course-7l6 – Matt Smeets Jun 16 '20 at 18:07
1

Well, it wasn't all that straightforward figuring this out. A lot of the docs are out of date or don't address this issue completely. This is what I got so far:

services.yaml:

Symfony\Component\Routing\RouterInterface:
    arguments: ['@router']

application.php:

#!/usr/bin/env php
<?php
// application.php

require __DIR__.'/vendor/autoload.php';
require __DIR__.'/src/Kernel.php';

use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Dotenv\Dotenv;

$dotenv = new Dotenv();
$dotenv->load(__DIR__.'/.env', __DIR__.'/.env.local');

$kernel = new App\Kernel(getenv('APP_ENV'), getenv('APP_DEBUG'));
$kernel->boot();

$container = $kernel->getContainer();

$application = new Application($kernel);

$application->add(new App\Command\MyCommand($container->get('router')));

$application->run();

Note: I changed the Application import to Symfony\Bundle\FrameworkBundle\Console\Application

MyCommand.php:

<?php
// src/Command/MyCommand.php
namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LockableTrait;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpClient\HttpClient;
use App\SQSHelper;

class MyCommand extends Command
{
    use LockableTrait;

    // the name of the command (the part after "bin/console")
    protected static $defaultName = 'app:my-command';

    protected $router;

    public function __construct(RouterInterface $router)
    {
        parent::__construct();
        $this->router = $router;
    }

    protected function configure()
    {
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if($this->lock()) { // Prevent running more than one instance
            $endpoint = 
            $queueName = 'Queue';
            $queue = new SQSHelper();

            while($queue->getApproxNumberOfMessages($queueName)) {
                $message = $queue->receiveMessage($queueName);
                if($message) {
                    if($message['__EOQ__'] ?? FALSE) // End-of-Queue marker received
                        break;

                    $context = $this->router->getContext();
                    $context->setHost('localhost');
                    $context->setHttpPort('49100');
                    $context->setHttpsPort('49100');
                    $context->setScheme('https');
                    $context->setBaseUrl('');

                    $url = $this->router->generate('ep', ['MessageId' => $message['MessageId']], UrlGeneratorInterface::ABSOLUTE_URL);

                    $client = HttpClient::create();
                    $response = $client->request('POST', $url, [
                        'headers' => ['Content-Type' => 'application/json'],
                        'body' => $message['Body'] // Already JSON encoded
                    ]);
                }
            }

            $this->release(); // Release lock

            // this method must return an integer number with the "exit status code"
            // of the command. You can also use these constants to make code more readable

            // return this if there was no problem running the command
            // (it's equivalent to returning int(0))
            return Command::SUCCESS;

            // or return this if some error happened during the execution
            // (it's equivalent to returning int(1))
            // return Command::FAILURE;
        }
    }
}

If anything feels off or if you could offer a better solution or improvements, please contribute...

Thanks Matt Smeets for your invaluable help figuring out there is no problem with the command, and if you can suggest a better alternative for the application.php, I'll accept your answer.

Thejaka Maldeniya
  • 1,076
  • 1
  • 10
  • 20
1

Solution introduced with Symfony 5.1 :

https://symfony.com/doc/current/routing.html#generating-urls-in-commands

Generating URLs in commands works the same as generating URLs in services. The only difference is that commands are not executed in the HTTP context. Therefore, if you generate absolute URLs, you’ll get http://localhost/ as the host name instead of your real host name.

The solution is to configure the default_uri option to define the “request context” used by commands when they generate URLs:

# config/packages/routing.yaml
framework:
    router:
        # ...
        default_uri: 'https://example.org/my/path/'
Guildem
  • 2,183
  • 1
  • 10
  • 13
  • 1
    Link only answers are discouraged: https://meta.stackexchange.com/a/8259 . Rather put the solution into your answer. – helvete Jun 28 '21 at 11:08