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?
Related
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
From a Service class, how manage exceptions and return an error 500 ?
By example, I have a service class 'A' called from another service Class 'B'. The content of the service 'A' is :
namespace App\Service;
use ...
class A
{
...
public static function foo(){
$tmp = [];
// do some stuff
if(isOK($tmp)){
return $tmp;
}else{
// return 500 with message
}
}
private static function isOK($tmp){
// do some stuff
}
}
I tried this :
namespace App\Service;
use ...
class A
{
...
public static function foo(){
$tmp = [];
// do some stuff
if(isOK($tmp)){
return $tmp;
}else{
// return 500 with message
}
}
private static function isOK($tmp){
try{
if(...) throw new \Exception();
}catch (HttpException $e){
dump('not valid data $tmp var in ' . __FUNCTION__,500);
exit;
}
}
}
But I don't think I use a good way. If I deliberately set a wrong value to the $tmp var, the process is stopped (as I want) and, in a case where I use this service for build a symfony http web page, a blank page is displayed with my message but this page get a status 200 (not a 500 'internal server error').
What is the good/properly way for return an exception from a Service ?
Is there a global (symfony ? oop php?) way for manage properly errors exceptions in the 'service called from another service' context and/or in the 'service called from a controller used only for REST web service' context and/or , more conventionally, in the 'service called from a classical http controller' context ? (bonus : and/or in the "service called from a custom Command Class")
Maybe I completely misunderstand the question, but I'd say: throw an Exception from your Service.
But: you only catch an Exception, if you can properly handle it. In your case it looks as if you can't handle it in your Service, so you let it bubble up its way to the appropriate Symfony component (that differs between Console command, Controller or Rest endpoint).
The Service shouldn't set the 500 code, as it doesn't know in which context it is used. Therefor you might want to throw an explicit ServiceException and catch that in your controller and convert it to something more useful:
class A
{
public function foo(){
$tmp = [];
if($this->isOK($tmp)){
return $tmp;
}
throw new ServiceException('Failed checking $tmp');
}
private function isOK($tmp){
return false;
}
}
class TestController
{
/**
* #var A
*/
protected $a;
public function fooAction() {
try {
$this->a->foo();
} catch (ServiceException $e) {
throw new HttpException(500, $e->getMessage())
}
}
}
For web and rest you have to make sure that your Exception has the correct code, which will then be used to set the HTTP code.
Only the code that uses the service knows how to handle the Exception properly. As the status code doesn't matter in your console command, you could not catch it.
But in general you can say that this is not best practice, as you might have to do some cleanup (close connections, close file handles, write error log) before the Exception is passed to the next code level.
An example for the console:
class MyCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$a = new A();
try {
$this->a->foo();
} catch (ServiceException $e) {
// write to log file
$io->error('Service failed: ' . $e->getMessage());
return;
}
// do more stuff
}
}
I have a File class, with a ->deleteOnExit function. If this function is called, the file will delete itself on shutdown. The File class uses the register_shutdown_function function to achieve this, deleting the file when the shutdown handler is ran. Question is, how can i test this in PHPUnit. I tried this
$file = new File(__DIR__ . "/delete_on_exit.txt");
$file->createFile();
$file->deleteOnExit();
register_shutdown_function(function() use ($file) {
$this->assertFalse($file->exists());
});
But that did not work. I guess that is because PHPUnit is already done with its testing by the time the function registered with register_shutdown_function is called.
Is there a way to do this?
You should probably use tmpfile() (documentation) internally instead of deleting the file with a shutdown function.
Also, the way PHPUnit works, you cannot test proper shutdown deletion. PHPUnit tests run in a single huge "application" - any shutdown function will only ever be called when the last test finishes and any other tasks like generating code coverage have been done. At this time, it is way too late for your test to report that the file indeed has been deleted.
Additionally, because of this I'd have my doubts whether or not your code will reliably delete the file, because any other registered shutdown function that simply calls exit() would prevent your shutdown function to delete the file. All these problems seem to not exist when using tmpfile(), because that is a OS supported call that will delete the file if the process opening it dies for whatever reason.
For the general purpose of unit testing php_register_callback_function, this is my hack :
<?php
namespace PAG\Testing;
class BlackboxedScriptRunner
{
public static $php = "/usr/bin/php";
public static function fetchScriptStdout($script): string
{
return self::executeFile($script, ' 2>/dev/null');
}
private static function executeFile($script, $option): string
{
if (php_sapi_name() !== "cli")
throw new RuntimeException("Cannot use this function in this setting for security reasons");
return shell_exec(self::$php ." -f $script -- $option");
}
public static function fetchScriptStderr($script): string
{
return self::executeFile($script, ' 2>&1 > /dev/null');
}
}
and the test :
<?php
use PAG\Testing\BlackboxedScriptRunner;
use PHPUnit\Framework\TestCase;
class ShutdownEventHandlerTest extends TestCase
{
public function testRegisterShutdownHandler()
{
$output =
BlackboxedScriptRunner::fetchScriptStdout(__DIR__ . "/../ManualTesting/testRegisterShutdownHandler.php");
$this->assertEquals(
file_get_contents(__DIR__ . "/../ManualTesting/testRegisterShutdownHandler.txt"),
$output);
}
}
Now I'm not saying this is awesome, I am saying this is awesome, I am saying it allows you to check the output of a whole script.
Warning : this is worse than eval, do not use for other purposes than testing.
And I am in a joyful mood, so here is my so called RegisterShutdownHandler :
<?php
namespace PAG\Shutdown;
class ShutdownEventHandler
{
private static $shutdown;
private static $shutdown_error;
private static $initialized = false;
private static $context = [];
public static function registerShutdownHandler($identifier, $function)
{
self::ensureInitialization();
self::$shutdown[$identifier] = $function;
}
private static function ensureInitialization(): void
{
if (!self::$initialized) {
self::$initialized = true;
self::registerShutdown();
}
}
private static function registerShutdown(): void
{
register_shutdown_function(function () {
self::shutdown();
});
}
private static function shutdown()
{
if (!is_null($error = error_get_last())) {
self::runCallbacks(self::$shutdown_error, $error);
}
self::runCallbacks(self::$shutdown);
}
private static function runCallbacks($array, $arguments = []): void
{
foreach ($array as $function) {
call_user_func($function, self::$context, $arguments);
}
}
public static function registerErrorShutdownHandler($identifier, $function)
{
self::ensureInitialization();
self::$shutdown_error[$identifier] = $function;
}
public static function deleteShutdownHandler($identifier)
{
unset(self::$shutdown[$identifier]);
}
public static function deleteErrorShutdownHandler($identifier)
{
unset(self::$shutdown_error[$identifier]);
}
public static function setContext(array $context): void
{
self::$context = $context;
}
}
You can find more of that on github. Must be frank though, it still has poor coverage.
The following code is a simplified example of what I'm trying to understand.
I'm using an external library that uses callbacks to process multiple requests. Ultimately I've been trying to figure out how to make Test->inc() call ExternalLib for each array element, then wait for all callbacks to be executed before continuing.
As you can see, the fatal error on line 18 is due to the method being called via call_user_func. How can I do this, or is there perhaps a better method?
class Test {
public $a = array();
public function inc(array $ints){
$request = new ExternalLib();
foreach ($ints as $int) {
$request->increment($int);
}
while( count($this->a) < count($ints) ) {
usleep(500000);
}
$test->dump();
}
public function incCallback($in, $out) {
/* append data to Test class array */
$this->a[] = array($in => out); /* Fatal error: Using $this when not in object context */
}
public function dump() {
/* Print to screen */
var_dump($this->a);
}
}
class ExternalLib {
/* This library's code should not be altered */
public function increment($num) {
call_user_func(array('Test','incCallback'), $num, $num++);
}
}
$test = new Test();
$test->inc(array(5,6,9));
Desired output:
array(3) {
[5]=>
int(6)
[6]=>
int(7)
[9]=>
int(10)
}
This code also available at codepad
The problem isn't a timing/waiting issue. It's a static vs. instantiated issue.
Calling the function using call_user_func(array('Test','incCallback')... is the same as calling Test::incCallback. You can't use $this when making a static call.
You will either need to modify the external lib to use an instantiated object or modify the Test class to use all static data.
Edit
I don't know exactly what you're looking to accomplish, but if making the class operate as a static class is your only option, then that's what you have to do ...
There are a couple other issues with your code:
Based on your desired output, you don't want a[] = array($in, $out) but rather a[$in] = $out
$num++ will not increment until after the function is called ... you want ++$num
Here is a working example ...
class Test {
public static $a;
public function inc(array $ints){
$request = new ExternalLib();
foreach ($ints as $int) {
$request->incriment($int);
}
while( count(self::$a) < count($ints) ) {}
self::dump();
}
public function incCallback($in, $out) {
/* append data to Test class array */
self::$a[$in] = $out;
}
public function dump() {
/* Print to screen */
var_dump(self::$a);
}
}
class ExternalLib {
/* This library's code should not be altered */
public function incriment($num) {
call_user_func(array('Test','incCallback'), $num, ++$num);
}
}
Test::$a = array();
Test::inc(array(5,6,9));
I'm trying to build a form wizard in Kohana and am learning a bit as I go. One of the things that I've learn might work best is utilizing a state pattern in my class structure to manage the different steps a user can be in during the form process.
After doing some research, I've been thinking that the best approach may be to use an interface and have all of the steps act as states that implement the interface. After a state validates, it will change a session variable to the next step, which can be read upon the initial load of the interface and call the correct state to use.
Does this approach make sense? If so, how the heck do I make it happen (how do I best structure the filesystem?)
Here is the rough start I've been working on:
<?php defined('SYSPATH') or die('No direct script access.');
/**
* Project_Builder #state
* Step_One #state
* Step_Two #state
**/
interface Project_Builder
{
public function do_this_first();
public function validate();
public function do_this_after();
}
class Step_One implements Project_Builder {
public function __construct
{
parent::__construct();
// Do validation and set a partial variable if valid
}
public function do_this_first()
{
echo 'First thing done';
// This should be used to set the session step variable, validate and add project data, and return the new view body.
$session->set('step', '2');
}
public function do_this_after()
{
throw new LogicException('Have to do the other thing first!');
}
}
class Step_Two implements Project_Builder {
public function do_this_first()
{
throw new LogicException('Already did this first!');
}
public function do_this_after()
{
echo 'Did this after the first!';
return $this;
}
}
class Project implements Project_Builder {
protected $state;
protected $user_step;
protected $project_data
public function __construct()
{
// Check the SESSION for a "step" entry. If it does not find one, it creates it, and sets it to "1".
$session = Session::instance('database');
if ( ! $session->get('step'))
{
$session->set('step', '1');
}
// Get the step that was requested by the client.
$this->user_step = $this->request->param('param1');
// Validate that the step is authorized by the session.
if ($session->get('step') !== $this->user_step)
{
throw new HTTP_Exception_404('You cannot skip a step!');
}
// Check if there is user data posted, and if so, clean it.
if (HTTP_Request::POST == $this->request->method())
{
foreach ($this->request->post() as $name => $value)
{
$this->project_data["$name"] = HTML::chars($value);
}
}
// Trigger the proper state to use based on the authorized session step (should I do this?)
$this->state = new Step_One;
}
public function doThisFirst()
{
$this->state = $this->state->do_this_first();
}
public function doThisAfter()
{
$this->state = $this->state->do_this_after();
}
}
$project = new Project;
try
{
$project->do_this_after(); //throws exception
}
catch(LogicException $e)
{
echo $e->getMessage();
}
$project = new Project;
$project->do_this_first();
$project->validate();
$project->do_this_after();
//$project->update();
Your way certainly looks possible, however I would be tempted to keep it simpler and use some of Kohanas build in features to take care of what you want. For example, I would use Kostache (mustache) and have separate View classes (and potentially templates) for each step. Then the controller becomes quite simple. See the example below (missing session stuff and validation of the step_number). All of the validation is handled in the model. If there is a validation error, an exception can be thrown which can then pass error messages back to the View.
<?php
class Wizard_Controller {
function action_step($step_number = 1)
{
$view = new View_Step('step_' + $step_number);
if ($_POST)
{
try
{
$model = new Model_Steps;
$model->step_number = $step_number;
if ($model->save($_POST))
{
// Go to the next step
$step_number++;
Request::current()->redirect('wizard/step/'.$step_number);
}
}
catch (Some_Kind_Of_Exception $e)
{
$view->post = $_POST;
$view->errors = $e->errors();
}
}
$view->render();
}
}
?>
Hope this makes sense.