In Symfony, I want to create a command that, when invoked, will run the method from the service - all operations will be performed there.
I would like the user to be able to see the progress of the operation.
So from the service methods I need to access the ProgressBar from the command.
On the Internet I found a solution consisting in sending a callback to the service that will operate on the progress bar.
Something like that:
// service method
public function update(\Closure $callback = null)
{
$validCountryCodes = $this->countryRepository->findAll();
$products = $this->productRepository->findWithInvalidCountryCode($validCountryCodes);
foreach ($products as $product) {
if ($callback) {
$callback($product);
}
...
}
}
/**
* command file
*/
public function execute(InputInterface $input, OutputInterface $output)
{
$progress = new ProgressBar($output, 50);
$progress->start();
$callback = function ($product) use ($progress) {
$progress->advance();
};
$this->getContainer()->get('update.product.countries')->update($callback);
}
It works, but the problem is that in the service the operation is split into many methods.
The question is how to make the callback passed to the update method available to all methods in servie?
I could pass it as a parameter to each method I run, but that doesn't look very elegant...
Thank you in advance for your help.
Regards
Related
I want to implement chain of responsibility pattern for authorization in my app. I have created four different chain services for authorization and they depends on which routes user want to access. I have a problem with chaining services. I want to chain services without explicitly naming them. For example:
class Authorization1:
public function auth($request){
if (isThisRoute){
$this->authorize($request);
}
$this->authorization2->authorize($request);
}
I want to know how can i replace last line: $this->authorization2->authorize($request); with $this->chain->authorize($request); so chain of responsibility pattern can be implemented completly.
Your code looks like Laravel middleware code.
Try to learn it: https://refactoring.guru/design-patterns/chain-of-responsibility
If your really want to use Chain of Responsibility instead of Strategy for your four different services it can look like the follow code (PHP v8.0).
Interface for chains
interface AuthorizationRequestHandlerInterface
{
public function auth(Request $request): bool;
// Option 2
public function setNext(AuthorizationRequestHandlerInterface $handler): void;
}
A handler for a condition or state
class Authorization1 implements AuthorizationRequestHandlerInterface
{
// Option 1
public function __construct(private AuthorizationRequestHandlerInterface $handler)
{
}
// Option 2
public function setNext(AuthorizationRequestHandlerInterface $handler): void
{
$this->handler = $handler;
}
public function auth(Request $request): bool
{
if (isThisRoute) {
// Check permissions here or call your private or protected method
return $this->authorize($request);
}
// Option 1: Call the next handler
return $this->handler->auth($request);
// Option 2: Call the next handler if it exists
return $this->handler ? $this->handler->auth($request) : false;
}
private function authorize(Request $request): bool
{
$result = false;
// Your code
return $result;
}
}
Other handlers looks like previous one.
You may do any operations with your object and return any type of value, but this example uses bool.
You should prepare configuration of your services in services.yml or another way which you use.
At the end PHP code may look like that:
// Initialization: Option 1
$authNull = new AuthNull();
$auth1 = new Authorization1($authNull);
$auth2 = new Authorization2($auth1);
$auth3 = new Authorization3($auth2);
return $auth3;
// Initialization: Option 2
$auth1 = new Authorization1($authNull);
$auth2 = new Authorization2($auth1);
$auth3 = new Authorization3($auth2);
$auth1->setNext($auth2);
$auth2->setNext($auth3);
// In this way you must add the `setNext` method and use its value as `handler` instead of that constructor value.
return $auth1;
// ...
// A class that has the main handler, like AbstractGuardAuthenticator, Controller, ArgumentResolver, ParamConverter, Middleware, etc.
if ($auth->handle($request)) {
// when it is authorizable, continues the request
} else {
// when it isn't authorizable, throws Exception, for example
}
// Different handling order that depends on the options.
// Option 1: Auth3 -> Auth2 -> Auth1 -> AuthNull
// Option 2: Auth1 -> Auth2 -> Auth3
As #alexcm mentioned, you should read some Symfony information:
https://symfony.com/doc/current/security/voters.html
https://symfony.com/doc/current/routing.html
https://symfony.com/doc/current/configuration.html
https://symfony.com/doc/current/components/http_kernel.html
https://symfony.com/doc/current/controller/argument_value_resolver.html
https://symfony.com/bundles/SensioFrameworkExtraBundle/current/annotations/converters.html
Is using computed include / require a bad code smell and does it have a bad impact on the performance? And I guess that having the included file execute code is also a bad thing to do, but is it ok if that behavior is documented?
Background information / Reason for my question:
I need to call an API to get information about some services. I have about 50 services with each service needing to call the API for 0-6 times. So I'm looking for a way to configure
The parameters for the API call (argumenttype differs between calls, may be a string but it also may be an array)
Define which API to call
I thought of having a single file for each service containing the calls and return the information as a single array like this:
<?php
// Params for Call 1
$anArrayWithSomeParams = array('param1', 'param2', 'param3', 'param4');
// Params for Call 2
$aString = 'string1';
$anotherString = 'string2'
// Params for Call 3-6
...
$someInformation = $dmy->getSomeInformation($anArrayWithSomeParams);
$notNeededHere = NULL;
$moreInformation = $dmy->getMoreInformation($aString,$anotherString);
...
$allData = array( '0' => $someInformation,
'1' => $notNeededHere
'2' => $tablespace,
....
);
?>
I then could include that file and use the variable alldata to access the data and do something with it like this:
require_once('class.dummy.php');
$directories = array("dir1", "dir2", "dir3");
$dmy = new dummy();
foreach($directories as $path) {
$allData = NULL;
$executeDataCollection = $path.'myFile.php';
require($executeDataCollection);
print "<pre>";
print_r($allData);
print "</pre>";
}
While this might work, it does not seem like an elegant solution. I was wondering if somebody could give me a hint towards a more elegant/sophisticated way of handling this.
Thanks in advance!
Using require and any of similiar approach is bad practice.
You should think more in OOP way how to implement this. To achieve something like this I would suggest to use interface and abstract class. In your case you need to call some APIS with different parameters on demand you should use following patterns/principles:
Adapter
Factory
Gateway
S.O.L.I.D - some of the principles will help you to design better what you need
Interface will look like:
interface ApiGateway {
/**
* This will execute call with optional parameters
*
**/
public function call($parameters = null);
}
Abstract class
abstract class ApiGatewayAbstract implements ApiGateway
{
/** return Adapter for handle API call **/
abstract protected function getAdapter();
/** return list of arguments for call */
abstract protected function getArguments();
public function call($parameters = null)
{
$adapter = $this->getAdapter();
$arguments = $this->getArguments();
// this will be HTTPAdapter for executing API call you need with specific params and arguments
return $adapter->execute($arguments, $parameters);
}
}
Now you can start implementing specific ApiGateways:
class MyApiGateway extends ApiGatewayAbstract
{
protected $arguments = [];
protected $adapter;
public function __construct(HttpClientInterface $httpClient, array $arguments = [])
{
$this->arguments = $arguments;
$this->adapter = $httpClient;
}
protected function getArguments()
{
return $this->arguments;
}
protected function getAdapter()
{
return $this->adapter;
}
}
Final step would be Factory for your ApiGateways:
class ApiGatewayFactory
{
// dynamic way to get Specific api gateway by name, or you can implement for each api gateway specific method
public function getApiGateway($name, HttpClientInterface $adapter, array $arguments)
{
$className = 'Namespace\\'.$name;
if (!class_exist($className) {
throw new \Exception('Unsuported ApiGateway');
}
// here you can use reflection or simply do:
return new $className($adapter, $arguments);
}
}
By this approach you will achieve clean way of what you want and also follow some of the principles from S.O.L.I.D. So you can add more ApiGateways with specific use cases, or different adapters ( soap, http, socket ) etc.
Hope this helps, also this is just an example have a look at the patterns and how to implement them. But this example should help you understand the approach.
Related: Error in PHP Generator
How do I propagate errors in a generator function without stopping iteration?
Example:
class SynchronizeSchedules extends \Symfony\Component\Console\Command\Command
{
protected function execute(InputInterface $input, OutputInterface $output): void {
$synchronizer = new \ScheduleSync();
foreach ($synchronizer->sync() as $schedule) {
$output->writeln("Schedule {$schedule->id} synchronized");
}
}
}
class ScheduleSync
{
public function sync(): \Generator {
$data = [/* data from somewhere */];
foreach ($data as $d) {
try {
// insert into database
$this->db->insert('...');
yield $d;
} catch (DBALException $e) {
// something went wrong
}
}
}
}
If a database error (DBALException) occurs, I want to do something. For example in the CLI command (Symfony Console) I'd like to write to STDOUT. When invoked in the web application, logging the error to a file or something would be more appropriate.
Apart from passing a LoggerInterface object into the class that has a generator method, is there a clean way of dealing with this?
I'm developing a pretty simple Symfony console application. It has just one command with one argument, and a few options.
I followed this guide to create an extension of the Application class.
This is the normal usage for the app, and it works fine:
php application <argument>
This also works fine (argument with options):
php application.php <argument> --some-option
If someone runs php application.php without any arguments or options, I want it to run as though the user had run php application.php --help.
I do have a working solution but it isn't optimal and is perhaps slightly brittle. In my extended Application class, I overrode the run() method as follows:
/**
* Override parent method so that --help options is used when app is called with no arguments or options
*
* #param InputInterface|null $input
* #param OutputInterface|null $output
* #return int
* #throws \Exception
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
{
if ($input === null) {
if (count($_SERVER["argv"]) <= 1) {
$args = array_merge($_SERVER["argv"], ["--help"]);
$input = new ArgvInput($args);
}
}
return parent::run($input, $output);
}
By default, Application::run() is called with a null InputInterface, so here I figured I could just check the raw value of the arguments and forcefully add a help option to pass to the parent method.
Is there a better way to achieve this?
I managed to work out a solution which didn't involve touching the Application class at all. To call the help command from within another command:
/**
* #param InputInterface $input
* #param OutputInterface $output
* #return int
* #throws \Symfony\Component\Console\Exception\ExceptionInterface
*/
protected function outputHelp(InputInterface $input, OutputInterface $output)
{
$help = new HelpCommand();
$help->setCommand($this);
return $help->run($input, $output);
}
To do a specific action depending on command, you can use an EventListener which is called when the onConsoleCommand is fired.
The listener class should work as follows :
<?php
namespace AppBundle\EventListener;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Command\HelpCommand;
class ConsoleEventListener
{
public function onConsoleCommand(ConsoleCommandEvent $event)
{
$application = $event->getCommand()->getApplication();
$inputDefinition = $application->getDefinition();
if ($inputDefinition->getArgumentCount() < 2) {
$help = new HelpCommand();
$help->setCommand($event->getCommand());
return $help->run($event->getInput(), $event->getOutput());
}
}
}
The service declaration :
services:
# ...
app.console_event_listener:
class: AppBundle\EventListener\ConsoleEventListener
tags:
- { name: kernel.event_listener, event: console.command, method: onConsoleCommand }
using the apache log4php and its being called by a helper method in my class:
class MyClass{
function log($msg, $level='info'){
$log = #Logger::getLogger("MyLogger");
$log->$level($msg);
}
}
The issue with the above code is the file:line column in logger will always report the line num for the help method. I can get the original line num and file that calls the helper method using php's debug_backtrace():
$bt = debug_backtrace();
$caller = array_shift($bt);
So my question is, is there a way within my helper method to set the file:line column? I'm thinking that I might need to overwrite a Logger::method or something?
My solution is a hack to the source code, which should be avoided. There is an answer by #Sven that should cover most scenarios but for me my log calls have to go through a helper method.
In the LoggerLoggingEvent.php class file add the method:
/**
* Set the event location info
* #param LoggerLocationInfo $locationInfo
*/
public function setLocationInformation(LoggerLocationInfo $locationInfo) {
$this->locationInfo = $locationInfo;
}
Then in your log class method use:
/**
* Log an INFO message
* #param string $msg The message to log
* #return none
*/
public function log($msg, $level='info'){
// Manually construct a logging event
$level = LoggerLevel::toLevel($level);
$logger = Logger::getLogger(__CLASS__);
$event = new LoggerLoggingEvent(__CLASS__, $logger, $level, $msg);
// Override the location info
$bt = debug_backtrace();
$caller = array_shift($bt);
$location = new LoggerLocationInfo($caller);
$event->setLocationInformation($location);
// Log it
$logger->logEvent($event);
}
The solution is not to have a log function on your own, but to use the logger right there where you now make the call to your own log function.
class MyClass {
public function __construct() {
$this->logger = Logger::getLogger("MyLogger");
}
public function anywhere() {
// $this->log("Foo"); // Don't do this,
$this->logger->info("Foo"); // do this.
}
}
A generic logging framework cannot know how many layers of indirection your log call really took, and strip these from the backtrace. You also loose the ability to pass exceptions to the logger.