What I have is a symfony application, which contains some entities along with some repositories. A second non-symfony application should interface with the first one for interacting with some logic written in it (in this very moment just using the entities and their proper repositories).
Keep in mind that the first application could have its own autoload register etc.
I thought of an API class for external applications, which stays in the app directory. To use that the application should require a script. Here is the idea:
app/authInterface.php that the external application should require:
$loader = require __DIR__.'/autoload.php';
require_once (__DIR__.'/APIAuth.php');
return new APIAuth();
and an example of a working APIAuth I wrote (the code is kind of messy: remember this is just a try but you can get the idea):
class APIAuth
{
public function __construct()
{
//dev_local is a personal configuration I'm using.
$kernel = new AppKernel('dev_local', false);
$kernel->loadClassCache();
$kernel->boot();
$doctrine = $kernel->getContainer()->get('doctrine');
$em = $doctrine->getManager();
$users = $em->getRepository('BelkaTestBundle:User')->findUsersStartingWith('thisisatry');
}
by calling it by the shell everything works and I'm happy with it:
php app/authInterface.php
but I'm wondering if I'm doing in the best way possible in terms of:
resources am I loading just the resources I really need to run my code? Do I really need the kernel? That way everything is properly loaded - including the DB connection- but I'm not that sure if there are other ways to do it lighter
symfony logics am I interacting with symfony the right way? Are there better ways?
Symfony allows using its features from the command line. If you use a CronJob or another application, and want to call your Symfony application, you have two general options:
Generating HTTP endpoints in your Symfony application
Generating a command which executes code in your Symfony application
Both options will be discussed below.
HTTP endpoint (REST API)
Create a route in your routing configuration to route a HTTP request to a Controller/Action.
# app/config/routing.yml
test_api:
path: /test/api/v1/{api_key}
defaults: { _controller: AppBundle:Api:test }
which will call the ApiController::testAction method.
Then, implement the testAction with your code you want to excecute:
use Symfony\Component\HttpFoundation\Response;
public function testAction() {
return new Response('Successful!');
}
Command
Create a command line command which does something in your application. This can be used to execute code which can use any Symfony service you have defined in your (web)application.
It might look like:
// src/AppBundle/Command/TestCommand.php
namespace AppBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class GreetCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('myapp:section:thecommand')
->setDescription('Test command')
->addArgument(
'optionname',
InputArgument::OPTIONAL,
'Test option'
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$option = $input->getArgument('optionname');
if ($option) {
$text = 'Test '.$option;
} else {
$text = 'Test!';
}
$output->writeln($text);
}
}
Look here for documentation.
Call your command using something like
bin/console myapp:section:thecommand --optionname optionvalue
(Use app/console for pre-3.0 Symfony installations.)
Use whichever option you think is best.
One word of advice. Do not try to use parts of the Symfony framework when your application is using the full Symfony framework. Most likely you will walk into trouble along the way and you're making your own life hard.
Use the beautiful tools you have at your disposal when you are already using Symfony to build your application.
Related
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']
Is there a simpler way to generate a URL from a Console Command by
explicitly specifying host, protocol, port, route, parameters etc?
Why isn't UrlGeneratorInterface or RouterInterface autowiring?
Do I need to specify wiring manually as $router.default in
services.yaml if I also have autowiring enabled?
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();
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.
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.
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/'
This question has been discussed many times here, here or here but no elegant solutions were mentioned.
One particular use case would be to allow to load and route old PHP files with Laravel. I am for instance migrating a very old (> 20 years) code base into Laravel and most pages are regular PHP files that I would like to render into a particular Blade template.
To do this it would be elegant to do:
Router::php('/some/route/{id}', base_path('legacy/some/page.php'));
Behind the scenes all I need is to pass the captured variables to the PHP page, evaluate and grab the content of it and eventually return a view instance.
As Laravel claims itself to be a SOLID framework, I thought extending the Router is trivial so I wrote this:
namespace App\Services;
class Router extends \Illuminate\Routing\Router
{
public function php($uri, $filename, $template='default') {
...
return view(...
}
}
Then I tried to extend my Http Kernel with this:
namespace App\Http;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use App\Services\Router;
class Kernel extends HttpKernel
{
public function __construct(Application $app, Router $router) {
return parent::__construct($app, $router);
}
}
But it is not working it seems the Application is building the Kernel with the wrong dependency. In Application#registerCoreContainerAliases I see the core alias router is hard coded and since this method is called in the Application's constructor, I am doomed.
The only solution that remains is to override the router before loading the Kernel as follow:
$app = new Application($_ENV['APP_BASE_PATH'] ?? dirname(__DIR__));
$app->singleton('router', \App\Services\Router::class);
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
But this looks a bit ugly. Is there a better way to achieve this?
Since the Router class is macroable, you may be able to do something like:
Router::macro('php', function ($uri, $filepath) {
return $this->addRoute(['GET', 'POST', etc...], $uri, function () use ($filepath) {
// here you might use the blade compiler to render the raw php along with any variables.
//
// See: https://laravel.com/api/5.7/Illuminate/View/Compilers/Concerns/CompilesRawPhp.html
//
$contents = file_get_contents($filepath);
// return compiled $contents...
});
});
I'm trying to scrape a page and I'm not very familiar with php frameworks, so I've been trying to learn Symfony2. I have it up and running, and now I'm trying to use Goutte. It's installed in the vendor folder, and I have a bundle I'm using for my scraping project.
Question is, is it good practice to do scraping from a Controller? And how? I have searched forever and cannot figure out how to use Goutte from a bundle, since it's buried deep withing the file structure.
<?php
namespace ontf\scraperBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Goutte\Client;
class ThingController extends Controller
{
public function somethingAction($something)
{
$client = new Client();
$crawler = $client->request('GET', 'http://www.symfony.com/blog/');
echo $crawler->text();
return $this->render('scraperBundle:Thing:index.html.twig');
// return $this->render('scraperBundle:Thing:index.html.twig', array(
// 'something' => $something
// ));
}
}
I'm not sure I have heard of "good practices" as far as scraping goes but you may be able to find some in the book PHP Architect's Guide to Web Scraping with PHP.
These are some guidelines I have used in my own projects:
Scraping is a slow process, consider delegating that task to a background process.
Background process normally run as a cron job that executing a CLI application or a worker that is constantly running.
Use a process control system to manage your workers. Take a look at supervisord
Save every scraped file (the "raw" version), and log every error. This will enable you to detect problems. Use Rackspace Cloud Files or AWS S3 to archive these files.
Use the Symfony2 Console tool to create the commands to run your scraper. You can save the commands in your bundle under the Command directory.
Run your Symfony2 commands using the following flags to prevent running out of memory: php app/console scraper:run example.com --env=prod --no-debug Where app/console is where the Symfony2 console applicaiton lives, scraper:run is the name of your command, example.com is an argument to indicate the page you want to scrape, and the --env=prod --no-debug are the flags you should use to run in production. see code below for example.
Inject the Goutte Client into your command like such:
Ontf/ScraperBundle/Resources/services.yml
services:
goutte_client:
class: Goutte\Client
scraperCommand:
class: Ontf\ScraperBundle\Command\ScraperCommand
arguments: ["#goutte_client"]
tags:
- { name: console.command }
And your command should look something like this:
<?php
// Ontf/ScraperBundle/Command/ScraperCommand.php
namespace Ontf\ScraperBundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Goutte\Client;
abstract class ScraperCommand extends Command
{
private $client;
public function __construct(Client $client)
{
$this->client = $client;
parent::__construct();
}
protected function configure()
{
->setName('scraper:run')
->setDescription('Run Goutte Scraper.')
->addArgument(
'url',
InputArgument::REQUIRED,
'URL you want to scrape.'
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$url = $input->getArgument('url');
$crawler = $this->client->request('GET', $url);
echo $crawler->text();
}
}
You Should take a Symfony-Controller if you want to return a response, e.G a html output.
if you only need the function for calculating or storing stuff in database,
You should create a Service class that represents the functionality of your Crawler, e.G
class CrawlerService
{
function getText($url){
$client = new Client();
$crawler = $client->request('GET', $url);
return $crawler->text();
}
and to execute it i would use a Console Command
If you want to return a Response use a Controller
I want to integrate Doctrine ORM into my (non-Symfony) project. I already done this in another project and used the famous cli-config.php into the project root directory.
But now, in my new project, I use the Symfony Console component and the Dependency Injection component (to reference services and commands, by tagging them).
1. I absolutely don't want to have a cli-config.php in the project root. How the Sf Doctrine Bundle do this?
2. Also (but it is less important), I would like to have the Doctrine commands into my project CLI.
What would be the best way to do this? Create references to Doctrine commands into my services.yml ? Or create local "decorator commands" that call Doctrine commands via PHP?
Finally, after some googling and experiments, I found a complete solution.
Just read the doctrine.php in vendor/bin. It is very easy to avoid the hardcoded config-cli.php file.
1. Create an entity manager
In my case, I use a factory and this method hydrates the doctrine.em service.
($config is specific to my app, change values to use your own logic.)
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;
public function createEntityManager()
{
$config = $this->get('config');
$metadataConfig = Setup::createAnnotationMetadataConfiguration(
[$config->meta('app_path') . '/Entity'],
$config->oc->doctrine->dev_mode
);
return EntityManager::create((array) $config->oc->doctrine->connection, $metadataConfig);
}
2. Merge Doctrine CLI commands in your CLI commands
Somewere in your code, like in some bootstrap.php, you probably declare your Symfony\Component\Console\Application command line interface, that's how I do this (the foreach simply adds commands defined in my services.yml file):
$application = new Application('MyApp CLI', '0.0.1');
$services = $container->findTaggedServiceIds('oc.command');
foreach(array_keys($services) as $serviceId)
{
$application->add($container->get($serviceId));
}
$application->run();
Now, we simply ask Doctrine to inject its commands into our Application:
$application = new Application('MyApp CLI', '0.0.1');
$helperSet = ConsoleRunner::createHelperSet($container->get('doctrine.em'));
$application->setHelperSet($helperSet);
ConsoleRunner::addCommands($application);
$services = $container->findTaggedServiceIds('oc.command');
foreach(array_keys($services) as $serviceId)
{
$application->add($container->get($serviceId));
}
$application->run();
That's it! You can also only add a subset of the Doctrine commands by using arsfeld's answer on this GitHub issue.
3. Bonus: only import needed commands and rename them
You can create decorator commands that inherit Doctrine commands (this is useful to redefine the name of Doctrine commands, as Symfony Doctrine Bundle does, eg. orm:validate-schema -> doctrine:schema:validate).
To do this, remove the line ConsoleRunner::addCommands($application); we added in step 2. For each command you want to redefine, you will need to create an register a new command in your app. This command will "extends" the target Doctrine command and will override the configure() method.
Here is an example with orm:validate-schema:
<?php
namespace MyApp\Command\Doctrine;
use Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand;
class SchemaValidateCommand extends ValidateSchemaCommand
{
protected function configure()
{
parent::configure();
$this->setName('doctrine:schema:validate');
}
}
Some Doctrine commands have aliases that will pollute your command namespaces, like orm:generate-entities and orm:generate:entities.
To remove these aliases, in configure(), add ->setAliases(array()).
$this->setName('doctrine:generate:entities')->setAliases([]);
Congratulations, you just redone the Symfony Doctrine Bundle :p (jk)
I would like to extend Laravels Router class (Illuminate\Routing\Router) to add a method I need a lot in my application.
But sadly I can't get this to work. I already extended other classes successfully so I really have no idea where my wrong thinking comes from.
Anyway, right into the code:
<?php
namespace MyApp\Extensions;
use Illuminate\Routing\Router as IlluminateRouter;
class Router extends IlluminateRouter
{
public function test()
{
$route = $this->getCurrentRoute();
return $route->getParameter('test');
}
}
So as you see I want to get the parameter set by {test} in routes.php with a simple call like:
Router::test();
Not sure how to go on now. Tried to bind it to the IOC-Container within my ServiceProvider in register() and boot() but I got no luck.
Whatever I try I get either a constructor error or something else.
All solutions I found are too old and the API has changed since then.
Please help me!
edit:
I already tried binding my own Router within register() and boot() (as said above) but it doesn't work.
Here is my code:
<?php
namespace MyApp;
use Illuminate\Support\ServiceProvider;
use MyApp\Extensions\Router;
class MyAppServiceProvider extends ServiceProvider {
public function register()
{
$this->app['router'] = $this->app->share(function($app)
{
return new Router(new Illuminate\Events\Dispatcher);
}
// Other bindings ...
}
}
When I try to use my Router now I have the problem that it needs an Dispatcher.
So I have to do:
$router = new Router(new Illuminate\Events\Dispatcher); // Else I get an exception :(
Also it simply does nothing, if I call:
$router->test();
:(
And if I call
dd($router->test());
I get NULL
Look at: app/config/app.php and in the aliases array. You will see Route is an alias for the illuminate router via a facade class.
If you look at the facade class in Support/Facades/Route.php of illuminate source, you will see that it uses $app['router'].
Unlike a lot of service providers in laravel, the router is hard coded and cannot be swapped out without a lot of work rewiring laravel or editing the vendor source (both are not a good idea). You can see its hardcoded by going to Illuminate / Foundation / Application.php and searching for RoutingServiceProvider.
However, there's no reason i can think of that would stop you overriding the router class in a service provider. So if you create a service provider for your custom router, which binds to $app['router'], that should replace the default router with your own router.
I wouldn't expect any issues to arise from this method, as the providers should be loaded before any routing is done. So overriding the router, should happen before laravel starts to use the router class, but i've not this before, so be prepared for a bit of debugging if it doesn't work straight away.
So I was asking in the official Laravel IRC and it seems like you simply can't extend Router in 4.1 anymore. At least that's all I got as a response in a pretty long dialogue.
It worked in Laravel 4.0, but now it doesn't. Oh well, maybe it will work in 4.2 again.
Other packages suffer from this as well: https://github.com/jasonlewis/enhanced-router/issues/16
Anyway, personally I'll stick with my extended Request then. It's not that much of a difference, just that Router would've been more dynamic and better fitting.
I'm using Laravel 4.2, and the router is really hard coded into the Application, but I extended it this way:
Edit bootstrap/start.php, change Illuminate\Foundation\Application for YourNamespace\Application.
Create a class named YourNamespace\Application and extend \Illuminate\Foundation\Application.
class Application extends \Illuminate\Foundation\Application {
/**
* Register the routing service provider.
*
* #return void
*/
protected function registerRoutingProvider()
{
$this->register(new RoutingServiceProvider($this));
}
}
Create a class named YourNamespace\RoutingServiceProvider and extend \Illuminate\Routing\RoutingServiceProvider.
class RoutingServiceProvider extends \Illuminate\Routing\RoutingServiceProvider {
protected function registerRouter()
{
$this->app['router'] = $this->app->share(function($app)
{
$router = new Router($app['events'], $app);
// If the current application environment is "testing", we will disable the
// routing filters, since they can be tested independently of the routes
// and just get in the way of our typical controller testing concerns.
if ($app['env'] == 'testing')
{
$router->disableFilters();
}
return $router;
});
}
}
Finally, create YourNamespace\Router extending \Illuminate\Routing\Router and you're done.
NOTE: Although you're not changing the name of the class, like Router and RoutingServiceProvider, it will work because of the namespace resolution that will point it to YourNamespace\Router and so on.