Using doctrine in Symfony2 command - symfony

I have a command connecting to an external database and loading the data into my application's database. The command will run periodically as a cron job. However, I run into the following problem when I run the command in the console:
PHP Fatal error: Call to undefined method Symfony\Component\Console\Application::getKernel() in E:\www\project\vendor\symfony\symfony\src\Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand.php on line 43
I followed the tutorial here on symfony's website to the letter.
Here's the service definition:
app.command.get_transactions:
class: AppBundle\Command\TransactionsCommand
arguments: [ #doctrine.orm.entity_manager ]
tags:
- { name: console.command }
Here's my command code:
<?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;
use AppBundle\Entity\Transaction;
use AppBundle\Entity\TransactionSync;
use Doctrine\DBAL\DriverManager;
class TransactionsCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('transactions:get')
->setDescription('Import transactions')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$em = $this->getContainer()->get('doctrine')->getManager();
$q = $em->createQueryBuilder();
$q->select('t')->from('AppBundle:TransactionSync', 't')->orderBy('t.id', 'DESC')->setMaxResults(1);
$sync = $q->getQuery()->getResult();
$em1 = $this->getContainer()->get('doctrine')->getManager('rnr');
$conn = $em1->getConnection();
$query = "SELECT id, merchant, client, phone, traderTransIdent AS member_id, transaction_id, transaction_type_id, value AS amount, points, DATE_FORMAT(STR_TO_DATE( transaction_date, '%d-%m-%Y' ), '%Y-%m-%d') AS transaction_date FROM merchant_transactions WHERE id > ". $sync->getId();
$stmt = $conn->prepare($query);
$stmt->execute();
$results = $stmt->fetchAll();
if(count($results) > 1)
{
$ts = new TransactionSync();
$ts->setStartTime(new \DateTime());
$id = 0;
foreach($results as $result)
{
$transaction_type = $em->getRepository('AppBundle:TransactionType')->find($result['transaction_type_id']);
$member = $em->getRepository('AppBundle:Member')->find($result['member_id']);
$transaction = new Transaction();
$transaction->setAmount($result['amount']);
$transaction->setPoints($result['points']);
$transaction->setClient($result['client']);
$transaction->setPhone($result['phone']);
$transaction->setTransactionId($result['transaction_id']);
$transaction->setTransactionDate(new \DateTime($result['transaction_date']));
$transaction->setTransactionType($transaction_type);
$transaction->setMember($member);
$em->persist($transaction);
$id = $result['id'];
}
$ts->setLastId($id);
$ts->setRecords(count($results));
$ts->setEndTime(new \DateTime());
$em->persist($ts);
$em->flush();
}
$output->writeln($text);
}
}
According to the accepted answer here and many other places online I have seen, extending ContainerAwareCommand should solve this but I still keep getting the error. Please assist in pointing the step I missed, I'll be very grateful

Remove your service definition, as you put your command inside Command folder and extended ContainerAwareCommand you don't need to use any tags and inject entity manager.

ContainerAware has been deprecated in 4.2. It's saying now:
The ContainerAwareCommand class has been deprecated. It was used in
the past to create commands extending from it so they had direct
access to the app service container. The alternative is to extend
commands from the Command class and use proper service injection in
the command constructor.
https://symfony.com/blog/new-in-symfony-4-2-important-deprecations

Related

Passing an attribute to Doctrine AbstractIdGenerator

I need to pass LoggerInterface to the MyGenerator used in #ORM\CustomIdGenerator(class=MyGenerator::class)
Doctrine does not use the symfony container to instantiate the generator and I'm ending up with an Exception Too few arguments to function How can I use the LoggerInterface in my id generator ?
Unfortunately, it's not possible to inject LoggerInterface into MyGenerator class, as it's not a service and has nothing to do with the service container. However, in AbstractIdGenerator there is an EntityManager available, which provides a foundation for a workaround solution in order to propagate logs via a database table. After that, you'll be able to fetch log messages from a table via cronjob and write proper logs or do whatever you need.
class MyGenerator extends AbstractIdGenerator
{
public function generate(EntityManager $em, $entity)
{
$identifier = '...'; // generate an identifier
// push a log message to a db
$query = $em->createQuery('INSERT INTO db.logger (id, message, created_at) VALUES (null, :message, NOW())');
$query->setParameter('message', 'Log message...');
$query->execute();
return $identifier;
}
}

Decorate all services that implement the same interface by default?

I have a growing number of service classes that share a common interface (let's say BarService and BazService, that implement FooInterface).
All of these need to be decorated with the same decorator. Reading the docs, I know that I can do:
services:
App\BarDecorator:
# overrides the App\BarService service
decorates: App\BarService
Since I have to use the same decorator for different services I guess I would need to do:
services:
bar_service_decorator:
class: App\BarDecorator
# overrides the App\BarService service
decorates: App\BarService
baz_service_decorator:
class: App\BarDecorator
# overrides the App\BazService service
decorates: App\BazService
Problem is: this gets repetitive, quickly. And every time a new implementation of FooInterface is created, another set needs to be added to the configuration.
How can I declare that I want to decorate all services that implement FooInterface automatically, without having to declare each one individually?
A compiler pass allows to modify the container programmatically, to alter service definitions or add new ones.
First you'll need a way to locate all implementations of FooInterface. You can do this with the help of autoconfigure:
services:
_instanceof:
App\FooInterface:
tags: ['app.bar_decorated']
Then you'll need to create the compiler pass that collects all FooServices and creates a new decorated definition:
// src/DependencyInjection/Compiler/FooInterfaceDecoratorPass.php
namespace App\DependencyInjection\Compiler;
use App\BarDecorator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class FooInterfaceDecoratorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has(BarDecorator::class)) {
// If the decorator isn't registered in the container you could register it here
return;
}
$taggedServices = $container->findTaggedServiceIds('app.bar_decorated');
foreach ($taggedServices as $id => $tags) {
// skip the decorator, we do it's not self-decorated
if ($id === BarDecorator::class) {
continue;
}
$decoratedServiceId = $this->generateAliasName($id);
// Add the new decorated service.
$container->register($decoratedServiceId, BarDecorator::class)
->setDecoratedService($id)
->setPublic(true)
->setAutowired(true);
}
}
/**
* Generate a snake_case service name from the service class name
*/
private function generateAliasName($serviceName)
{
if (false !== strpos($serviceName, '\\')) {
$parts = explode('\\', $serviceName);
$className = end($parts);
$alias = strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($className)));
} else {
$alias = $serviceName;
}
return $alias . '_decorator';
}
}
Finally, register the compiler pass in the kernel:
// src/Kernel.php
use App\DependencyInjection\Compiler\FooInterfaceDecoratorPass;
class Kernel extends BaseKernel
{
// ...
protected function build(ContainerBuilder $container)
{
$container->addCompilerPass(new FooInterfaceDecoratorPass());
}
}
Interesting! I think that's going to be tricky... but maybe with some hints here you might come up with a solution that fits your needs
find all Decorators... not sure if there's an easier way in that case but I use tags for that. So create a DecoratorInterface add auto tag it...
loop through the definitions and and modify and set the decorated service
e. g. in your Kernel or AcmeAwesomeBundle do
protected function build(ContainerBuilder $container)
{
$container->registerForAutoconfiguration(DecoratorInterface::class)
->addTag('my.decorator.tag');
$decoratorIds = $container->findTaggedServiceIds('my.decorator.tag');
foreach ($decoratorIds as $decoratorId) {
$definition = $container->getDefinition($decoratorId);
$decoratedServiceId = $this->getDecoratedServiceId($definition);
$definition->setDecoratedService($decoratedServiceId);
}
}
private function getDecoratedServiceId(Definition $decoratorDefinition): string
{
// todo
// maybe u can use the arguments here
// e.g. the first arg is always the decoratedService
// might not work because the arguments are not resolved yet?
$arg1 = $decoratorDefinition->getArgument(0);
// or use a static function in your DecoratorInterface like
// public static function getDecoratedServiceId():string;
$class = $decoratorDefinition->getClass();
$decoratedServiceId = $class::getDecoratedServiceId();
return 'myDecoratedServiceId';
}
I'm pretty sure this is not complete yet but let us know how you solved it

Symfony router call controller

I'm writing my own project and want to use some of Symfony components.
I'm implementing Sf Router to my project and I don't understand how to call controller in router.
For ex I have a class:
<?php
namespace App;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\HttpFoundation as Http;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
class AppRouter
{
public function match() {
$context = new RequestContext();
$request = Http\Request::createFromGlobals();
$context->fromRequest($request);
$matcher = new UrlMatcher($this->getRoutes(), $context);
return $matcher->matchRequest($request);
}
private function getRoutes() {
$fileLocator = new FileLocator([__DIR__ . '/../config']);
$loader = new YamlFileLoader($fileLocator);
$routes = $loader->load('routes.yml');
return $routes;
}
}
and I have some defined routes in my routes.yml so if I trying to go to registred route match() method returns to me an array like below:
[
"_controller" => "Controller\\MyController::testAction"
"_route" => "test"
]
So, how now can I call the controller for matched URL? I'm read a documentation but can't understand how should I do this.
This is a much more involved question then it might look.
The easiest approach is to simply explode the _controller string, new the controller and call the action.
// Untested but I think the syntax is correct
$matched = $router->match();
$parts = explode(':',$matched['_controller']);
$controller = new $parts[0]();
$controller->$parts[2]();
Of course there is a great deal of error checking to be added.
The reason I say it can be more complicated is that there is actually a lot you more you can do. Take a look at the HTTP Component. HttpKernel::handle($request) which uses a ControllerResolver class to create a controller as well as an ArgumentResolver to handle many of the controller's arguments. Fun stuff.
Create Your Own Framework is an excellent resource.
And the fairly new Symfony Flex approach gives you a bare bones framework which is worth studying as well.
The HTTP Kernel handles the Route to Controller invokation. You can find the full documentation here
The Controller Resolver and argument resolver will help you to match pretty easily a route to a controller method. This is the easy and robust way in order to get the router up and running on a legacy project without having to invoke the fully-fledge framework.
Think about it once more: our framework is more robust and more flexible than ever and it still has less than 50 lines of code.
require_once __DIR__.'/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;
use Symfony\Component\HttpKernel;
function render_template(Request $request)
{
extract($request->attributes->all(), EXTR_SKIP);
ob_start();
include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
return new Response(ob_get_clean());
}
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
$controllerResolver = new HttpKernel\Controller\ControllerResolver();
$argumentResolver = new HttpKernel\Controller\ArgumentResolver();
try {
$request->attributes->add($matcher->match($request->getPathInfo()));
$controller = $controllerResolver->getController($request);
$arguments = $argumentResolver->getArguments($request, $controller);
$response = call_user_func_array($controller, $arguments);
} catch (Routing\Exception\ResourceNotFoundException $exception) {
$response = new Response('Not Found', 404);
} catch (Exception $exception) {
$response = new Response('An error occurred', 500);
}
$response->send();
`
My solution for are next:
change a little my routes.yml parameters: no class name contain only slash
use call_user_func_array($this->controller, $this->parameters)

How to set session flash message in commad symfony2?

I've a file in testBundle>Command>ReportCommand.php where I want to set flash message like below but it's not working. I've also added this namespace but it didn't work too:-use Symfony\Component\HttpFoundation\Request;
$this->get('session')->getFlashBag()->add(
'notice', sprintf('%s email sent!', str_replace('_', ' ', ucfirst($type)))
);
You cannot use sessions from command line, you can only use them with the HTTP way. Try to store your message in a différent way :
In a file
In your MySQL database
In a RAM cache (E.g. redis)
etc...
You can use outer interface to show the message on command prompt.
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class classname extends ContainerAwareCommand {
protected function configure()
{
// command details
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// your script code
$output->writeln("Your message");
}
}

Alter service (ClientManager) based on configuration inside bundle extension class

I have a bundle named: "ApiBundle". In this bundle I have the class "ServiceManager", this class is responsible for retrieving a specific Service object. Those Service objects needs to be created based on some configuration, so after this piece of code in my bundle extension class:
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
// Create Service objects...
I create those Service objects right after I have processed the configuration, something like this:
foreach ($services as $name => $service) {
$service = new Service();
$service->setName($name);
$manager = $container->get($this->getAlias() . '.service_manager');
$manager->add($service);
}
Unfortunately, this does not work, probably because the container isn't compiled yet. So I tried to add those Service objects the following way:
$manager = $container->getDefinition($this->getAlias() . '.service_manager');
$manager->addMethodCall('add', array($service));
But again, this throws the following exception: RuntimeException: Unable to dump a service container if a parameter is an object or a resource.
I can't seem to get a grasp on how to use the service container correctly. Does someone knows how I can add those Service objects to the ServiceManager (which is a service) inside the bundle extension class?
This is how the configuration of the bundle looks like:
api_client:
services:
some_api:
endpoint: http://api.yahoo.com
some_other_api:
endpoint: http://api.google.com
Every 'service' will be a seperate Service object.
I hope I explained it well enough, my apologies if my english is incorrect.
Steffen
EDIT
I think I may have solved the problem, I made a Compiler Pass to manipulate the container there with the following:
public function process(ContainerBuilder $container)
{
$services = $container->getParameter('mango_api.services');
foreach ($services as $name => $service) {
$clientManager = $container->getDefinition('mango_api.client_manager');
$client = new Definition('Mango\Bundle\ApiBundle\Client\Client', array($name, 'client', 'secret'));
$container->setDefinition('mango_api.client.' .$name, $client);
$clientManager->addMethodCall('add', array($client));
}
}
Is this appropriate?
To create services based on configuration you need to create compiler pass and enable it.
Compiler passes give you an opportunity to manipulate other service
definitions that have been registered with the service container.
I think I may have solved the problem, I made a Compiler Pass to manipulate the container there with the following:
public function process(ContainerBuilder $container)
{
$services = $container->getParameter('mango_api.services');
foreach ($services as $name => $service) {
$clientManager = $container->getDefinition('mango_api.client_manager');
$client = new Definition('Mango\Bundle\ApiBundle\Client\Client', array($name, 'client', 'secret'));
$client->setPublic(false);
$container->setDefinition('mango_api.client.' .$name, $client);
$clientManager->addMethodCall('add', array($client));
}
}

Resources