diff --git a/CHANGELOG-3.2.md b/CHANGELOG-3.2.md index 1a4d44e199..544ae4b071 100644 --- a/CHANGELOG-3.2.md +++ b/CHANGELOG-3.2.md @@ -7,6 +7,22 @@ in 3.2 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v3.2.0...v3.2.1 +* 3.2.13 (2017-08-01) + + * bug #22244 [Console] Fix passing options with defaultCommand (Jakub Sacha) + * bug #23684 [Debug] Missing escape in debug output (c960657) + * bug #23654 [DI] Fix using private services in expressions (nicolas-grekas) + * bug #23662 [VarDumper] Adapt to php 7.2 changes (nicolas-grekas) + * bug #23649 [Form][TwigBridge] Don't render _method in form_rest() for a child form (fmarchalemisys) + * bug #23023 [DoctrineBridge][PropertyInfo] Added support for Doctrine Embeddables (vudaltsov) + * bug #23619 [Validator] Fix IbanValidator for ukrainian IBANs (paroe) + * bug #23586 Fix case sensitive sameSite cookie (mikefrancis) + * bug #23238 [Security] ensure the 'route' index is set before attempting to use it (gsdevme) + * bug #23330 [WebProfilerBundle] Fix full sized dump hovering in toolbar (ogizanagi) + * bug #23580 Fix login redirect when referer contains a query string (fabpot) + * bug #23558 [FrameworkBundle] fix ValidatorCacheWarmer: use serializing ArrayAdapter (dmaicher) + * bug #23574 [VarDumper] Move locale sniffing to dump() time (nicolas-grekas) + * 3.2.12 (2017-07-17) * bug #23549 [PropertyInfo] conflict for phpdocumentor/reflection-docblock 3.2 (xabbuh) diff --git a/UPGRADE-3.4.md b/UPGRADE-3.4.md index 1f15a45d18..7164e789a8 100644 --- a/UPGRADE-3.4.md +++ b/UPGRADE-3.4.md @@ -114,6 +114,35 @@ FrameworkBundle class has been deprecated and will be removed in 4.0. Use the `Symfony\Component\Translation\DependencyInjection\TranslatorPass` class instead. +HttpKernel +---------- + + * Relying on convention-based commands discovery has been deprecated and + won't be supported in 4.0. Use PSR-4 based service discovery instead. + + Before: + + ```yml + # app/config/services.yml + services: + # ... + + # implicit registration of all commands in the `Command` folder + ``` + + After: + + ```yml + # app/config/services.yml + services: + # ... + + # explicit commands registration + AppBundle\Command: + resource: '../../src/AppBundle/Command/*' + tags: ['console.command'] + ``` + Process ------- diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index e791d38277..eee3d21609 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -164,8 +164,8 @@ DependencyInjection * Using unsupported options to configure service aliases raises an exception. - * Setting or unsetting a private service with the `Container::set()` method is - no longer supported. Only public services can be set or unset. + * Setting or unsetting a service with the `Container::set()` method is + no longer supported. Only synthetic services can be set or unset. * Checking the existence of a private service with the `Container::has()` method is no longer supported and will return `false`. @@ -455,6 +455,32 @@ HttpFoundation HttpKernel ---------- + * Relying on convention-based commands discovery is not supported anymore. + Use PSR-4 based service discovery instead. + + Before: + + ```yml + # app/config/services.yml + services: + # ... + + # implicit registration of all commands in the `Command` folder + ``` + + After: + + ```yml + # app/config/services.yml + services: + # ... + + # explicit commands registration + AppBundle\Command: + resource: '../../src/AppBundle/Command/*' + tags: ['console.command'] + ``` + * Removed the `kernel.root_dir` parameter. Use the `kernel.project_dir` parameter instead. diff --git a/src/Symfony/Bundle/DebugBundle/DebugBundle.php b/src/Symfony/Bundle/DebugBundle/DebugBundle.php index 335ec5abd2..b00c06af78 100644 --- a/src/Symfony/Bundle/DebugBundle/DebugBundle.php +++ b/src/Symfony/Bundle/DebugBundle/DebugBundle.php @@ -27,8 +27,10 @@ class DebugBundle extends Bundle $container = $this->container; // This code is here to lazy load the dump stack. This default - // configuration for CLI mode is overridden in HTTP mode on - // 'kernel.request' event + // configuration is overridden in CLI mode on 'console.command' event. + // The dump data collector is used by default, so dump output is sent to + // the WDT. In a CLI context, if dump is used too soon, the data collector + // will buffer it, and release it at the end of the script. VarDumper::setHandler(function ($var) use ($container) { $dumper = $container->get('data_collector.dump'); $cloner = $container->get('var_dumper.cloner'); diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index d360615a12..81f1117a16 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -24,6 +24,7 @@ CHANGELOG 3.4.0 ----- + * Added support for `EventSubscriberInterface` on `MicroKernelTrait` * Removed `doctrine/cache` from the list of required dependencies in `composer.json` * Deprecated `validator.mapping.cache.doctrine.apc` service * The `symfony/stopwatch` dependency has been removed, require it via `composer diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index b1c893ad05..09c61933ee 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -11,6 +11,9 @@ namespace Symfony\Bundle\FrameworkBundle\Console; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\Console\Application as BaseApplication; use Symfony\Component\Console\Command\Command; @@ -30,6 +33,7 @@ class Application extends BaseApplication { private $kernel; private $commandsRegistered = false; + private $registrationErrors = array(); /** * Constructor. @@ -70,9 +74,25 @@ class Application extends BaseApplication $this->setDispatcher($this->kernel->getContainer()->get('event_dispatcher')); + if ($this->registrationErrors) { + $this->renderRegistrationErrors($input, $output); + } + return parent::doRun($input, $output); } + /** + * {@inheritdoc} + */ + protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) + { + if ($this->registrationErrors) { + $this->renderRegistrationErrors($input, $output); + } + + return parent::doRunCommand($command, $input, $output); + } + /** * {@inheritdoc} */ @@ -138,7 +158,13 @@ class Application extends BaseApplication foreach ($this->kernel->getBundles() as $bundle) { if ($bundle instanceof Bundle) { - $bundle->registerCommands($this); + try { + $bundle->registerCommands($this); + } catch (\Exception $e) { + $this->registrationErrors[] = $e; + } catch (\Throwable $e) { + $this->registrationErrors[] = new FatalThrowableError($e); + } } } @@ -149,9 +175,30 @@ class Application extends BaseApplication if ($container->hasParameter('console.command.ids')) { foreach ($container->getParameter('console.command.ids') as $id) { if (false !== $id) { - $this->add($container->get($id)); + try { + $this->add($container->get($id)); + } catch (\Exception $e) { + $this->registrationErrors[] = $e; + } catch (\Throwable $e) { + $this->registrationErrors[] = new FatalThrowableError($e); + } } } } } + + private function renderRegistrationErrors(InputInterface $input, OutputInterface $output) + { + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + (new SymfonyStyle($input, $output))->warning('Some commands could not be registered.'); + + foreach ($this->registrationErrors as $error) { + $this->doRenderException($error, $output); + } + + $this->registrationErrors = array(); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php index fec5b72a5b..ef632ca04c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php @@ -13,6 +13,7 @@ namespace Symfony\Bundle\FrameworkBundle\Kernel; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Routing\RouteCollectionBuilder; /** @@ -68,6 +69,13 @@ trait MicroKernelTrait ), )); + if ($this instanceof EventSubscriberInterface) { + $container->register('kernel', static::class) + ->setSynthetic(true) + ->addTag('kernel.event_subscriber') + ; + } + $this->configureContainer($container, $loader); $container->addObjectResource($this); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php index 25511142c9..ce7ebad1f1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php @@ -15,8 +15,13 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\ApplicationTester; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpKernel\KernelInterface; class ApplicationTest extends TestCase { @@ -130,6 +135,36 @@ class ApplicationTest extends TestCase $this->assertSame($newCommand, $application->get('example')); } + public function testRunOnlyWarnsOnUnregistrableCommand() + { + $container = new ContainerBuilder(); + $container->register('event_dispatcher', EventDispatcher::class); + $container->register(ThrowingCommand::class, ThrowingCommand::class); + $container->setParameter('console.command.ids', array(ThrowingCommand::class => ThrowingCommand::class)); + + $kernel = $this->getMockBuilder(KernelInterface::class)->getMock(); + $kernel + ->method('getBundles') + ->willReturn(array($this->createBundleMock( + array((new Command('fine'))->setCode(function (InputInterface $input, OutputInterface $output) { $output->write('fine'); })) + ))); + $kernel + ->method('getContainer') + ->willReturn($container); + + $application = new Application($kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'fine')); + $output = $tester->getDisplay(); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertContains('Some commands could not be registered.', $output); + $this->assertContains('throwing', $output); + $this->assertContains('fine', $output); + } + private function getKernel(array $bundles, $useDispatcher = false) { $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); @@ -189,3 +224,11 @@ class ApplicationTest extends TestCase return $bundle; } } + +class ThrowingCommand extends Command +{ + public function __construct() + { + throw new \Exception('throwing'); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php index 02bed82382..5337050d0e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php @@ -15,22 +15,37 @@ use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Routing\RouteCollectionBuilder; -class ConcreteMicroKernel extends Kernel +class ConcreteMicroKernel extends Kernel implements EventSubscriberInterface { use MicroKernelTrait; private $cacheDir; + public function onKernelException(GetResponseForExceptionEvent $event) + { + if ($event->getException() instanceof Danger) { + $event->setResponse(Response::create('It\'s dangerous to go alone. Take this ⚔')); + } + } + public function halloweenAction() { return new Response('halloween'); } + public function dangerousAction() + { + throw new Danger(); + } + public function registerBundles() { return array( @@ -57,6 +72,7 @@ class ConcreteMicroKernel extends Kernel protected function configureRoutes(RouteCollectionBuilder $routes) { $routes->add('/', 'kernel:halloweenAction'); + $routes->add('/danger', 'kernel:dangerousAction'); } protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) @@ -68,4 +84,18 @@ class ConcreteMicroKernel extends Kernel $c->setParameter('halloween', 'Have a great day!'); $c->register('halloween', 'stdClass'); } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + KernelEvents::EXCEPTION => 'onKernelException', + ); + } +} + +class Danger extends \RuntimeException +{ } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php index 5fde7f27f8..2cb1ba8f7d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php @@ -28,4 +28,15 @@ class MicroKernelTraitTest extends TestCase $this->assertEquals('Have a great day!', $kernel->getContainer()->getParameter('halloween')); $this->assertInstanceOf('stdClass', $kernel->getContainer()->get('halloween')); } + + public function testAsEventSubscriber() + { + $kernel = new ConcreteMicroKernel('test', true); + $kernel->boot(); + + $request = Request::create('/danger'); + $response = $kernel->handle($request); + + $this->assertSame('It\'s dangerous to go alone. Take this ⚔', $response->getContent()); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/doctrine.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/doctrine.yml new file mode 100644 index 0000000000..7a12388398 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Acl/doctrine.yml @@ -0,0 +1,5 @@ +# to be removed once https://github.com/doctrine/DoctrineBundle/pull/684 is merged +services: + Doctrine\Bundle\DoctrineBundle\Command\: + resource: "@DoctrineBundle/Command/*" + tags: [console.command] diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php index 1aab514f45..82c8ad4aa6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\app; +use Doctrine\ORM\Version; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Kernel; @@ -82,6 +83,11 @@ class AppKernel extends Kernel public function registerContainerConfiguration(LoaderInterface $loader) { $loader->load($this->rootConfig); + + // to be removed once https://github.com/doctrine/DoctrineBundle/pull/684 is merged + if ('Acl' === $this->testCase && class_exists(Version::class)) { + $loader->load(__DIR__.'/Acl/doctrine.yml'); + } } public function serialize() diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig index 9d03015f2f..84112fad6e 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig @@ -80,7 +80,7 @@ header .container { display: flex; justify-content: space-between; } .exception-hierarchy .icon svg { height: 13px; width: 13px; vertical-align: -2px; } .exception-without-message .exception-message-wrapper { display: none; } -.exception-message-wrapper .container { display: flex; align-items: flex-start; min-height: 70px; padding: 10px 0 8px; } +.exception-message-wrapper .container { display: flex; align-items: flex-start; min-height: 70px; padding: 10px 15px 8px; } .exception-message { flex-grow: 1; } .exception-message, .exception-message a { color: #FFF; font-size: 21px; font-weight: 400; margin: 0; } .exception-message.long { font-size: 18px; } @@ -107,11 +107,11 @@ header .container { display: flex; justify-content: space-between; } .trace-line .icon svg { height: 16px; width: 16px; } .trace-line-header { padding-left: 36px; } -.trace-file-path, .trace-file-path a { color: #999; color: #795da3; color: #B0413E; color: #222; font-size: 13px; } +.trace-file-path, .trace-file-path a { color: #222; font-size: 13px; } .trace-class { color: #B0413E; } .trace-type { padding: 0 2px; } -.trace-method { color: #B0413E; color: #222; font-weight: bold; color: #B0413E; } -.trace-arguments { color: #222; color: #999; font-weight: normal; color: #795da3; color: #777; padding-left: 2px; } +.trace-method { color: #B0413E; font-weight: bold; } +.trace-arguments { color: #777; font-weight: normal; padding-left: 2px; } .trace-code { background: #FFF; font-size: 12px; margin: 10px 10px 2px 10px; padding: 10px; overflow-x: auto; white-space: nowrap; } .trace-code ol { margin: 0; float: left; } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig index 6b8ba44dac..6153637026 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig @@ -6,7 +6,7 @@ {% set icon %} {{ include('@WebProfiler/Icon/validator.svg') }} - {{ collector.violationsCount }} + {{ collector.violationsCount ?: collector.calls|length }} {% endset %} diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 198d3268cd..5e04798c2a 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -698,6 +698,16 @@ class Application { $output->writeln('', OutputInterface::VERBOSITY_QUIET); + $this->doRenderException($e, $output); + + if (null !== $this->runningCommand) { + $output->writeln(sprintf('%s', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET); + $output->writeln('', OutputInterface::VERBOSITY_QUIET); + } + } + + protected function doRenderException(\Exception $e, OutputInterface $output) + { do { $title = sprintf( ' [%s%s] ', @@ -755,11 +765,6 @@ class Application $output->writeln('', OutputInterface::VERBOSITY_QUIET); } } while ($e = $e->getPrevious()); - - if (null !== $this->runningCommand) { - $output->writeln(sprintf('%s', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET); - $output->writeln('', OutputInterface::VERBOSITY_QUIET); - } } /** diff --git a/src/Symfony/Component/Console/EventListener/ErrorListener.php b/src/Symfony/Component/Console/EventListener/ErrorListener.php index 8e35d97dfd..3774f9e666 100644 --- a/src/Symfony/Component/Console/EventListener/ErrorListener.php +++ b/src/Symfony/Component/Console/EventListener/ErrorListener.php @@ -59,10 +59,10 @@ class ErrorListener implements EventSubscriberInterface } if (!$inputString = $this->getInputString($event)) { - return $this->logger->error('The console exited with code "{code}"', array('code' => $exitCode)); + return $this->logger->debug('The console exited with code "{code}"', array('code' => $exitCode)); } - $this->logger->error('Command "{command}" exited with code "{code}"', array('command' => $inputString, 'code' => $exitCode)); + $this->logger->debug('Command "{command}" exited with code "{code}"', array('command' => $inputString, 'code' => $exitCode)); } public static function getSubscribedEvents() diff --git a/src/Symfony/Component/Console/Tests/EventListener/ErrorListenerTest.php b/src/Symfony/Component/Console/Tests/EventListener/ErrorListenerTest.php index c857a97d0b..17eaae0908 100644 --- a/src/Symfony/Component/Console/Tests/EventListener/ErrorListenerTest.php +++ b/src/Symfony/Component/Console/Tests/EventListener/ErrorListenerTest.php @@ -61,7 +61,7 @@ class ErrorListenerTest extends TestCase $logger = $this->getLogger(); $logger ->expects($this->once()) - ->method('error') + ->method('debug') ->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255)) ; @@ -74,7 +74,7 @@ class ErrorListenerTest extends TestCase $logger = $this->getLogger(); $logger ->expects($this->never()) - ->method('error') + ->method('debug') ; $listener = new ErrorListener($logger); @@ -97,7 +97,7 @@ class ErrorListenerTest extends TestCase $logger = $this->getLogger(); $logger ->expects($this->exactly(3)) - ->method('error') + ->method('debug') ->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run --foo=bar', 'code' => 255)) ; @@ -112,7 +112,7 @@ class ErrorListenerTest extends TestCase $logger = $this->getLogger(); $logger ->expects($this->once()) - ->method('error') + ->method('debug') ->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255)) ; diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index 68b0c68c4e..d059ea477b 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -27,10 +27,12 @@ class DebugClassLoader private $classLoader; private $isFinder; private static $caseCheck; - private static $internal = array(); private static $final = array(); private static $finalMethods = array(); private static $deprecated = array(); + private static $deprecatedMethods = array(); + private static $internal = array(); + private static $internalMethods = array(); private static $darwinCache = array('/' => array('/', array())); /** @@ -165,50 +167,6 @@ class DebugClassLoader throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name)); } - $parent = get_parent_class($class); - $doc = $refl->getDocComment(); - if (preg_match('#\n \* @internal(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $doc, $notice)) { - self::$internal[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; - } - - // Not an interface nor a trait - if (class_exists($name, false)) { - if (preg_match('#\n \* @final(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $doc, $notice)) { - self::$final[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; - } - - if ($parent && isset(self::$final[$parent])) { - @trigger_error(sprintf('The "%s" class is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $parent, self::$final[$parent], $name), E_USER_DEPRECATED); - } - - // Inherit @final annotations - self::$finalMethods[$name] = $parent && isset(self::$finalMethods[$parent]) ? self::$finalMethods[$parent] : array(); - - foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) { - if ($method->class !== $name) { - continue; - } - - if ($parent && isset(self::$finalMethods[$parent][$method->name])) { - @trigger_error(sprintf('%s It may change without further notice as of its next major version. You should not extend it from "%s".', self::$finalMethods[$parent][$method->name], $name), E_USER_DEPRECATED); - } - - $doc = $method->getDocComment(); - if (false === $doc || false === strpos($doc, '@final')) { - continue; - } - - if (preg_match('#\n\s+\* @final(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) { - $message = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; - self::$finalMethods[$name][$method->name] = sprintf('The "%s::%s()" method is considered final%s.', $name, $method->name, $message); - } - } - } - - if (preg_match('#\n \* @deprecated (.*?)\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) { - self::$deprecated[$name] = preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]); - } - // Don't trigger deprecations for classes in the same vendor if (2 > $len = 1 + (strpos($name, '\\', 1 + strpos($name, '\\')) ?: strpos($name, '_'))) { $len = 0; @@ -224,37 +182,89 @@ class DebugClassLoader } } - foreach (array_merge(array($parent), class_implements($name, false), class_uses($name, false)) as $use) { + // Detect annotations on the class + if (false !== $doc = $refl->getDocComment()) { + foreach (array('final', 'deprecated', 'internal') as $annotation) { + if (false !== strpos($doc, '@'.$annotation) && preg_match('#\n \* @'.$annotation.'(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $doc, $notice)) { + self::${$annotation}[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; + } + } + } + + $parentAndTraits = class_uses($name, false); + if ($parent = get_parent_class($class)) { + $parentAndTraits[] = $parent; + + if (isset(self::$final[$parent])) { + @trigger_error(sprintf('The "%s" class is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $parent, self::$final[$parent], $name), E_USER_DEPRECATED); + } + } + + // Detect if the parent is annotated + foreach ($parentAndTraits + $this->getOwnInterfaces($name, $parent) as $use) { + if (isset(self::$deprecated[$use]) && strncmp($ns, $use, $len)) { + $type = class_exists($name, false) ? 'class' : (interface_exists($name, false) ? 'interface' : 'trait'); + $verb = class_exists($use, false) || interface_exists($name, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses'); + + @trigger_error(sprintf('The "%s" %s %s "%s" that is deprecated%s.', $name, $type, $verb, $use, self::$deprecated[$use]), E_USER_DEPRECATED); + } if (isset(self::$internal[$use]) && strncmp($ns, $use, $len)) { @trigger_error(sprintf('The "%s" %s is considered internal%s. It may change without further notice. You should not use it from "%s".', $use, class_exists($use, false) ? 'class' : (interface_exists($use, false) ? 'interface' : 'trait'), self::$internal[$use], $name), E_USER_DEPRECATED); } } - if (!$parent || strncmp($ns, $parent, $len)) { - if ($parent && isset(self::$deprecated[$parent]) && strncmp($ns, $parent, $len)) { - @trigger_error(sprintf('The "%s" class extends "%s" that is deprecated %s', $name, $parent, self::$deprecated[$parent]), E_USER_DEPRECATED); + // Inherit @final and @deprecated annotations for methods + self::$finalMethods[$name] = array(); + self::$deprecatedMethods[$name] = array(); + self::$internalMethods[$name] = array(); + foreach ($parentAndTraits as $use) { + foreach (array('finalMethods', 'deprecatedMethods', 'internalMethods') as $property) { + if (isset(self::${$property}[$use])) { + self::${$property}[$name] = array_merge(self::${$property}[$name], self::${$property}[$use]); + } + } + } + + $isClass = class_exists($name, false); + foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) { + if ($method->class !== $name) { + continue; } - $parentInterfaces = array(); - $deprecatedInterfaces = array(); - if ($parent) { - foreach (class_implements($parent) as $interface) { - $parentInterfaces[$interface] = 1; + // Method from a trait + if ($method->getFilename() !== $refl->getFileName()) { + continue; + } + + if ($isClass && $parent && isset(self::$finalMethods[$parent][$method->name])) { + list($declaringClass, $message) = self::$finalMethods[$parent][$method->name]; + @trigger_error(sprintf('The "%s::%s()" method is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $declaringClass, $method->name, $message, $name), E_USER_DEPRECATED); + } + + foreach ($parentAndTraits as $use) { + if (isset(self::$deprecatedMethods[$use][$method->name])) { + list($declaringClass, $message) = self::$deprecatedMethods[$use][$method->name]; + if (strncmp($ns, $declaringClass, $len)) { + @trigger_error(sprintf('The "%s::%s()" method is deprecated%s. You should not extend it from "%s".', $declaringClass, $method->name, $message, $name), E_USER_DEPRECATED); + } + } + if (isset(self::$internalMethods[$use][$method->name])) { + list($declaringClass, $message) = self::$internalMethods[$use][$method->name]; + if (strncmp($ns, $declaringClass, $len)) { + @trigger_error(sprintf('The "%s::%s()" method is considered internal%s. It may change without further notice. You should not extend it from "%s".', $declaringClass, $method->name, $message, $name), E_USER_DEPRECATED); + } } } - foreach ($refl->getInterfaceNames() as $interface) { - if (isset(self::$deprecated[$interface]) && strncmp($ns, $interface, $len)) { - $deprecatedInterfaces[] = $interface; - } - foreach (class_implements($interface) as $interface) { - $parentInterfaces[$interface] = 1; - } + // Detect method annotations + if (false === $doc = $method->getDocComment()) { + continue; } - foreach ($deprecatedInterfaces as $interface) { - if (!isset($parentInterfaces[$interface])) { - @trigger_error(sprintf('The "%s" %s "%s" that is deprecated %s', $name, $refl->isInterface() ? 'interface extends' : 'class implements', $interface, self::$deprecated[$interface]), E_USER_DEPRECATED); + foreach (array('final', 'deprecated', 'internal') as $annotation) { + if (false !== strpos($doc, '@'.$annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) { + $message = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; + self::${$annotation.'Methods'}[$name][$method->name] = array($name, $message); } } } @@ -357,4 +367,31 @@ class DebugClassLoader return true; } } + + /** + * `class_implements` includes interfaces from the parents so we have to manually exclude them. + * + * @param string $class + * @param string|false $parent + * + * @return string[] + */ + private function getOwnInterfaces($class, $parent) + { + $ownInterfaces = class_implements($class, false); + + if ($parent) { + foreach (class_implements($parent, false) as $interface) { + unset($ownInterfaces[$interface]); + } + } + + foreach ($ownInterfaces as $interface) { + foreach (class_implements($interface) as $interface) { + unset($ownInterfaces[$interface]); + } + } + + return $ownInterfaces; + } } diff --git a/src/Symfony/Component/Debug/ExceptionHandler.php b/src/Symfony/Component/Debug/ExceptionHandler.php index d84cfdd496..0ecd2a5347 100644 --- a/src/Symfony/Component/Debug/ExceptionHandler.php +++ b/src/Symfony/Component/Debug/ExceptionHandler.php @@ -320,11 +320,11 @@ EOF; .trace-message { font-size: 14px; font-weight: normal; margin: .5em 0 0; } - .trace-file-path, .trace-file-path a { margin-top: 3px; color: #999; color: #795da3; color: #B0413E; color: #222; font-size: 13px; } + .trace-file-path, .trace-file-path a { color: #222; margin-top: 3px; font-size: 13px; } .trace-class { color: #B0413E; } .trace-type { padding: 0 2px; } - .trace-method { color: #B0413E; color: #222; font-weight: bold; color: #B0413E; } - .trace-arguments { color: #222; color: #999; font-weight: normal; color: #795da3; color: #777; padding-left: 2px; } + .trace-method { color: #B0413E; font-weight: bold; } + .trace-arguments { color: #777; font-weight: normal; padding-left: 2px; } @media (min-width: 575px) { .hidden-xs-down { display: initial; } diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index 9045117cb9..1f06e5a091 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -223,6 +223,28 @@ class DebugClassLoaderTest extends TestCase $this->assertSame($xError, $lastError); } + public function testExtendedDeprecatedMethod() + { + set_error_handler(function () { return false; }); + $e = error_reporting(0); + trigger_error('', E_USER_NOTICE); + + class_exists('Test\\'.__NAMESPACE__.'\\ExtendsAnnotatedClass', true); + + error_reporting($e); + restore_error_handler(); + + $lastError = error_get_last(); + unset($lastError['file'], $lastError['line']); + + $xError = array( + 'type' => E_USER_DEPRECATED, + 'message' => 'The "Symfony\Component\Debug\Tests\Fixtures\AnnotatedClass::deprecatedMethod()" method is deprecated since version 3.4. You should not extend it from "Test\Symfony\Component\Debug\Tests\ExtendsAnnotatedClass".', + ); + + $this->assertSame($xError, $lastError); + } + public function testInternalsUse() { $deprecations = array(); @@ -235,9 +257,10 @@ class DebugClassLoaderTest extends TestCase restore_error_handler(); $this->assertSame($deprecations, array( - 'The "Symfony\Component\Debug\Tests\Fixtures\InternalClass" class is considered internal since version 3.4. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternals".', - 'The "Symfony\Component\Debug\Tests\Fixtures\InternalInterface" interface is considered internal. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternals".', + 'The "Symfony\Component\Debug\Tests\Fixtures\InternalClass" class is considered internal since version 3.4. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternalsParent".', + 'The "Symfony\Component\Debug\Tests\Fixtures\InternalInterface" interface is considered internal. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternalsParent".', 'The "Symfony\Component\Debug\Tests\Fixtures\InternalTrait" trait is considered internal. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternals".', + 'The "Symfony\Component\Debug\Tests\Fixtures\InternalTrait2::internalMethod()" method is considered internal since version 3.4. It may change without further notice. You should not extend it from "Test\Symfony\Component\Debug\Tests\ExtendsInternals".', )); } } @@ -281,10 +304,18 @@ class ClassLoader eval('namespace Test\\'.__NAMESPACE__.'; class Float {}'); } elseif ('Test\\'.__NAMESPACE__.'\ExtendsFinalClass' === $class) { eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsFinalClass extends \\'.__NAMESPACE__.'\Fixtures\FinalClass {}'); - } elseif ('Test\\'.__NAMESPACE__.'\ExtendsInternals' === $class) { - eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsInternals extends \\'.__NAMESPACE__.'\Fixtures\InternalClass implements \\'.__NAMESPACE__.'\Fixtures\InternalInterface { - use \\'.__NAMESPACE__.'\Fixtures\InternalTrait; + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsAnnotatedClass' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsAnnotatedClass extends \\'.__NAMESPACE__.'\Fixtures\AnnotatedClass { + public function deprecatedMethod() { } }'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsInternals' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsInternals extends ExtendsInternalsParent { + use \\'.__NAMESPACE__.'\Fixtures\InternalTrait; + + public function internalMethod() { } + }'); + } elseif ('Test\\'.__NAMESPACE__.'\ExtendsInternalsParent' === $class) { + eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsInternalsParent extends \\'.__NAMESPACE__.'\Fixtures\InternalClass implements \\'.__NAMESPACE__.'\Fixtures\InternalInterface { }'); } } } diff --git a/src/Symfony/Component/Debug/Tests/Fixtures/AnnotatedClass.php b/src/Symfony/Component/Debug/Tests/Fixtures/AnnotatedClass.php new file mode 100644 index 0000000000..dff9517d0a --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Fixtures/AnnotatedClass.php @@ -0,0 +1,13 @@ +properties = $properties; @@ -196,11 +203,24 @@ class Definition return $this; } + /** + * Gets the properties to define when creating the service. + * + * @return array + */ public function getProperties() { return $this->properties; } + /** + * Sets a specific property. + * + * @param string $name + * @param mixed $value + * + * @return $this + */ public function setProperty($name, $value) { $this->properties[$name] = $value; @@ -223,7 +243,7 @@ class Definition } /** - * Sets a specific argument. + * Replaces a specific argument. * * @param int|string $index * @param mixed $argument @@ -251,6 +271,14 @@ class Definition return $this; } + /** + * Sets a specific argument. + * + * @param int|string $key + * @param mixed $value + * + * @return $this + */ public function setArgument($key, $value) { $this->arguments[$key] = $value; @@ -757,7 +785,7 @@ class Definition } /** - * Sets autowired. + * Enables/disables autowiring. * * @param bool $autowired * diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index e2e271738a..d9a3d36fc1 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -12,7 +12,9 @@ namespace Symfony\Component\DependencyInjection\Dumper; use Symfony\Component\Yaml\Dumper as YmlDumper; +use Symfony\Component\Yaml\Parser; use Symfony\Component\Yaml\Tag\TaggedValue; +use Symfony\Component\Yaml\Yaml; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; @@ -267,6 +269,8 @@ class YamlDumper extends Dumper return $this->getParameterCall((string) $value); } elseif ($value instanceof Expression) { return $this->getExpressionCall((string) $value); + } elseif ($value instanceof Definition) { + return new TaggedValue('service', (new Parser())->parse("_:\n".$this->addService('_', $value), Yaml::PARSE_CUSTOM_TAGS)['_']['_']); } elseif (is_object($value) || is_resource($value)) { throw new RuntimeException('Unable to dump a service container if a parameter is an object or a resource.'); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php index 22277c7b7a..bae948feee 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php @@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Dumper\YamlDumper; use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Parser; @@ -64,6 +65,19 @@ class YamlDumperTest extends TestCase $this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services24.yml', $dumper->dump()); } + public function testInlineServices() + { + $container = new ContainerBuilder(); + $container->register('foo', 'Class1') + ->addArgument((new Definition('Class2')) + ->addArgument(new Definition('Class2')) + ) + ; + + $dumper = new YamlDumper($container); + $this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services_inline.yml', $dumper->dump()); + } + private function assertEqualYamlStructure($expected, $yaml, $message = '') { $parser = new Parser(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_inline.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_inline.yml new file mode 100644 index 0000000000..14adedf32d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_inline.yml @@ -0,0 +1,14 @@ + +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + synthetic: true + foo: + class: Class1 + arguments: [!service { class: Class2, arguments: [!service { class: Class2 }] }] + Psr\Container\ContainerInterface: + alias: service_container + public: false + Symfony\Component\DependencyInjection\ContainerInterface: + alias: service_container + public: false diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index 7ffa984017..3c32871c4d 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -141,7 +141,7 @@ class BinaryFileResponse extends Response */ public function setAutoEtag() { - $this->setEtag(substr(base64_encode(hash_file('sha256', $this->file->getPathname(), true)), 0, 32)); + $this->setEtag(base64_encode(hash_file('sha256', $this->file->getPathname(), true))); return $this; } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcacheSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcacheSessionHandler.php index 962a3878d9..4e490a05d4 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcacheSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcacheSessionHandler.php @@ -95,7 +95,9 @@ class MemcacheSessionHandler implements \SessionHandlerInterface */ public function destroy($sessionId) { - return $this->memcache->delete($this->prefix.$sessionId); + $this->memcache->delete($this->prefix.$sessionId); + + return true; } /** diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php index 76b08e2db9..67a49ad6f5 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php @@ -101,7 +101,9 @@ class MemcachedSessionHandler implements \SessionHandlerInterface */ public function destroy($sessionId) { - return $this->memcached->delete($this->prefix.$sessionId); + $result = $this->memcached->delete($this->prefix.$sessionId); + + return $result || $this->memcached->getResultCode() == \Memcached::RES_NOTFOUND; } /** diff --git a/src/Symfony/Component/HttpKernel/Bundle/Bundle.php b/src/Symfony/Component/HttpKernel/Bundle/Bundle.php index 0b0ea088dc..d0ea99f5fa 100644 --- a/src/Symfony/Component/HttpKernel/Bundle/Bundle.php +++ b/src/Symfony/Component/HttpKernel/Bundle/Bundle.php @@ -191,6 +191,8 @@ abstract class Bundle implements BundleInterface } $r = new \ReflectionClass($class); if ($r->isSubclassOf('Symfony\\Component\\Console\\Command\\Command') && !$r->isAbstract() && !$r->getConstructor()->getNumberOfRequiredParameters()) { + @trigger_error(sprintf('Auto-registration of the command "%s" is deprecated since Symfony 3.4 and won\'t be supported in 4.0. Use PSR-4 based service discovery instead.', $class), E_USER_DEPRECATED); + $application->add($r->newInstance()); } } diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 134bb97219..4f17d3c37c 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -24,6 +24,7 @@ CHANGELOG 3.4.0 ----- + * deprecated commands auto registration * added `AddCacheClearerPass` * added `AddCacheWarmerPass` diff --git a/src/Symfony/Component/HttpKernel/Tests/Bundle/BundleTest.php b/src/Symfony/Component/HttpKernel/Tests/Bundle/BundleTest.php index eeefccab81..8e52b097d6 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Bundle/BundleTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Bundle/BundleTest.php @@ -31,6 +31,10 @@ class BundleTest extends TestCase ); } + /** + * @group legacy + * @expectedDeprecation Auto-registration of the command "Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\Command\FooCommand" is deprecated since Symfony 3.4 and won't be supported in 4.0. Use PSR-4 based service discovery instead. + */ public function testRegisterCommands() { $cmd = new FooCommand(); diff --git a/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php b/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php index 1c913f7b8a..6fc7a48c97 100644 --- a/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php +++ b/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php @@ -35,36 +35,51 @@ trait BlockingStoreTestTrait public function testBlockingLocks() { // Amount a microsecond used to order async actions - $clockDelay = 200000; + $clockDelay = 50000; /** @var StoreInterface $store */ $store = $this->getStore(); $key = new Key(uniqid(__METHOD__, true)); + $parentPID = posix_getpid(); - if ($childPID1 = pcntl_fork()) { - // give time to fork to start - usleep(1 * $clockDelay); + // Block SIGHUP signal + pcntl_sigprocmask(SIG_BLOCK, array(SIGHUP)); + + if ($childPID = pcntl_fork()) { + // Wait the start of the child + pcntl_sigwaitinfo(array(SIGHUP), $info); try { - // This call should failed given the lock should already by acquired by the child #1 + // This call should failed given the lock should already by acquired by the child $store->save($key); $this->fail('The store saves a locked key.'); } catch (LockConflictedException $e) { } + // send the ready signal to the child + posix_kill($childPID, SIGHUP); + // This call should be blocked by the child #1 $store->waitAndSave($key); $this->assertTrue($store->exists($key)); $store->delete($key); // Now, assert the child process worked well - pcntl_waitpid($childPID1, $status1); + pcntl_waitpid($childPID, $status1); $this->assertSame(0, pcntl_wexitstatus($status1), 'The child process couldn\'t lock the resource'); } else { + // Block SIGHUP signal + pcntl_sigprocmask(SIG_BLOCK, array(SIGHUP)); try { $store->save($key); - // Wait 2 ClockDelay to let parent process to finish - usleep(2 * $clockDelay); + // send the ready signal to the parent + posix_kill($parentPID, SIGHUP); + + // Wait for the parent to be ready + pcntl_sigwaitinfo(array(SIGHUP), $info); + + // Wait ClockDelay to let parent assert to finish + usleep($clockDelay); $store->delete($key); exit(0); } catch (\Exception $e) { diff --git a/src/Symfony/Component/VarDumper/Caster/DateCaster.php b/src/Symfony/Component/VarDumper/Caster/DateCaster.php index c967bedb64..718b966390 100644 --- a/src/Symfony/Component/VarDumper/Caster/DateCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/DateCaster.php @@ -27,7 +27,7 @@ class DateCaster $fromNow = (new \DateTime())->diff($d); $title = $d->format('l, F j, Y') - ."\n".$fromNow->format('%R').self::formatInterval($fromNow).' from now' + ."\n".self::formatInterval($fromNow).' from now' .($location ? ($d->format('I') ? "\nDST On" : "\nDST Off") : '') ; diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php index ec85040839..b65376a630 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php @@ -27,42 +27,45 @@ class DateCasterTest extends TestCase /** * @dataProvider provideDateTimes */ - public function testDumpDateTime($time, $timezone, $expected) + public function testDumpDateTime($time, $timezone, $xDate, $xTimestamp) { $date = new \DateTime($time, new \DateTimeZone($timezone)); $xDump = <<assertDumpMatchesFormat($xDump, $date); + $this->assertDumpEquals($xDump, $date); } - public function testCastDateTime() + /** + * @dataProvider provideDateTimes + */ + public function testCastDateTime($time, $timezone, $xDate, $xTimestamp, $xInfos) { + if ((defined('HHVM_VERSION_ID') || PHP_VERSION_ID <= 50509) && preg_match('/[-+]\d{2}:\d{2}/', $timezone)) { + $this->markTestSkipped('DateTimeZone GMT offsets are supported since 5.5.10. See https://github.com/facebook/hhvm/issues/5875 for HHVM.'); + } + $stub = new Stub(); - $date = new \DateTime('2017-08-30 00:00:00.000000', new \DateTimeZone('Europe/Zurich')); + $date = new \DateTime($time, new \DateTimeZone($timezone)); $cast = DateCaster::castDateTime($date, array('foo' => 'bar'), $stub, false, 0); - $xDump = <<<'EODUMP' + $xDump = << 2017-08-30 00:00:00.0 Europe/Zurich (+02:00) + "\\x00~\\x00date" => $xDate ] EODUMP; - $this->assertDumpMatchesFormat($xDump, $cast); + $this->assertDumpEquals($xDump, $cast); - $xDump = <<<'EODUMP' + $xDump = <<assertDumpMatchesFormat($xDump, $interval, Caster::EXCLUDE_VERBOSE); + $this->assertDumpEquals($xDump, $interval, Caster::EXCLUDE_VERBOSE); } /** @@ -140,7 +144,7 @@ array:1 [ ] EODUMP; - $this->assertDumpMatchesFormat($xDump, $cast); + $this->assertDumpEquals($xDump, $cast); if (null === $xSeconds) { return; @@ -159,7 +163,7 @@ Symfony\Component\VarDumper\Caster\ConstStub { } EODUMP; - $this->assertDumpMatchesFormat($xDump, $cast["\0~\0interval"]); + $this->assertDumpEquals($xDump, $cast["\0~\0interval"]); } public function provideIntervals() @@ -214,7 +218,7 @@ DateTimeZone { } EODUMP; - $this->assertDumpMatchesFormat($xDump, $timezone, Caster::EXCLUDE_VERBOSE); + $this->assertDumpEquals($xDump, $timezone, Caster::EXCLUDE_VERBOSE); } /** @@ -233,7 +237,7 @@ array:1 [ ] EODUMP; - $this->assertDumpMatchesFormat($xDump, $cast); + $this->assertDumpEquals($xDump, $cast); $xDump = <<