Dependency updates and update version number

This commit is contained in:
Kode
2018-06-13 19:35:28 +01:00
parent 18ec208381
commit e3ec7de23a
1261 changed files with 45582 additions and 29687 deletions

View File

@@ -218,12 +218,11 @@ class Command
if (null !== $this->processTitle) {
if (function_exists('cli_set_process_title')) {
if (false === @cli_set_process_title($this->processTitle)) {
if (!@cli_set_process_title($this->processTitle)) {
if ('Darwin' === PHP_OS) {
$output->writeln('<comment>Running "cli_get_process_title" as an unprivileged user is not supported on MacOS.</comment>');
} else {
$error = error_get_last();
trigger_error($error['message'], E_USER_WARNING);
cli_set_process_title($this->processTitle);
}
}
} elseif (function_exists('setproctitle')) {

View File

@@ -21,7 +21,7 @@ use Symfony\Component\Console\Output\OutputInterface;
interface DescriptorInterface
{
/**
* Describes an InputArgument instance.
* Describes an object if supported.
*
* @param OutputInterface $output
* @param object $object

View File

@@ -121,7 +121,7 @@ class JsonDescriptor extends Descriptor
{
return array(
'name' => '--'.$option->getName(),
'shortcut' => $option->getShortcut() ? '-'.implode('|-', explode('|', $option->getShortcut())) : '',
'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '',
'accept_value' => $option->acceptValue(),
'is_value_required' => $option->isValueRequired(),
'is_multiple' => $option->isArray(),

View File

@@ -70,7 +70,7 @@ class MarkdownDescriptor extends Descriptor
{
$name = '--'.$option->getName();
if ($option->getShortcut()) {
$name .= '|-'.implode('|-', explode('|', $option->getShortcut())).'';
$name .= '|-'.str_replace('|', '|-', $option->getShortcut()).'';
}
$this->write(

View File

@@ -224,7 +224,7 @@ class XmlDescriptor extends Descriptor
$pos = strpos($option->getShortcut(), '|');
if (false !== $pos) {
$objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos));
$objectXML->setAttribute('shortcuts', '-'.implode('|-', explode('|', $option->getShortcut())));
$objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut()));
} else {
$objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : '');
}

View File

@@ -40,10 +40,10 @@ class ErrorListener implements EventSubscriberInterface
$error = $event->getError();
if (!$inputString = $this->getInputString($event)) {
return $this->logger->error('An error occurred while using the console. Message: "{message}"', array('error' => $error, 'message' => $error->getMessage()));
return $this->logger->error('An error occurred while using the console. Message: "{message}"', array('exception' => $error, 'message' => $error->getMessage()));
}
$this->logger->error('Error thrown while running command "{command}". Message: "{message}"', array('error' => $error, 'command' => $inputString, 'message' => $error->getMessage()));
$this->logger->error('Error thrown while running command "{command}". Message: "{message}"', array('exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()));
}
public function onConsoleTerminate(ConsoleTerminateEvent $event)

View File

@@ -305,7 +305,7 @@ class QuestionHelper extends Helper
foreach ($autocomplete as $value) {
// If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
if (0 === strpos($value, $ret) && $i !== strlen($value)) {
if (0 === strpos($value, $ret)) {
$matches[$numMatches++] = $value;
}
}

View File

@@ -282,7 +282,11 @@ class ArgvInput extends Input
return false;
}
foreach ($values as $value) {
if ($token === $value || 0 === strpos($token, $value.'=')) {
// Options with values:
// For long options, test for '--option=' at beginning
// For short options, test for '-o' at beginning
$leading = 0 === strpos($value, '--') ? $value.'=' : $value;
if ($token === $value || '' !== $leading && 0 === strpos($token, $leading)) {
return true;
}
}
@@ -306,13 +310,16 @@ class ArgvInput extends Input
}
foreach ($values as $value) {
if ($token === $value || 0 === strpos($token, $value.'=')) {
if (false !== $pos = strpos($token, '=')) {
return substr($token, $pos + 1);
}
if ($token === $value) {
return array_shift($tokens);
}
// Options with values:
// For long options, test for '--option=' at beginning
// For short options, test for '-o' at beginning
$leading = 0 === strpos($value, '--') ? $value.'=' : $value;
if ('' !== $leading && 0 === strpos($token, $leading)) {
return substr($token, strlen($leading));
}
}
}

View File

@@ -114,7 +114,7 @@ class ArrayInput extends Input
$params[] = $param.('' != $val ? '='.$this->escapeToken($val) : '');
}
} else {
$params[] = is_array($val) ? array_map(array($this, 'escapeToken'), $val) : $this->escapeToken($val);
$params[] = is_array($val) ? implode(' ', array_map(array($this, 'escapeToken'), $val)) : $this->escapeToken($val);
}
}

View File

@@ -33,6 +33,8 @@ interface InputInterface
*
* This method is to be used to introspect the input parameters
* before they have been validated. It must be used carefully.
* Does not necessarily return the correct result for short options
* when multiple flags are combined in the same option.
*
* @param string|array $values The values to look for in the raw parameters (can be an array)
* @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal
@@ -46,6 +48,8 @@ interface InputInterface
*
* This method is to be used to introspect the input parameters
* before they have been validated. It must be used carefully.
* Does not necessarily return the correct result for short options
* when multiple flags are combined in the same option.
*
* @param string|array $values The value(s) to look for in the raw parameters (can be an array)
* @param mixed $default The default value to return if no result is found

View File

@@ -195,7 +195,7 @@ class InputOption
*
* @return bool
*/
public function equals(InputOption $option)
public function equals(self $option)
{
return $option->getName() === $this->getName()
&& $option->getShortcut() === $this->getShortcut()

View File

@@ -83,21 +83,34 @@ class StreamOutput extends Output
*
* Colorization is disabled if not supported by the stream:
*
* - Windows != 10.0.10586 without Ansicon, ConEmu or Mintty
* - non tty consoles
* This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo
* terminals via named pipes, so we can only check the environment.
*
* Reference: Composer\XdebugHandler\Process::supportsColor
* https://github.com/composer/xdebug-handler
*
* @return bool true if the stream supports colorization, false otherwise
*/
protected function hasColorSupport()
{
if (DIRECTORY_SEPARATOR === '\\') {
return
'10.0.10586' === PHP_WINDOWS_VERSION_MAJOR.'.'.PHP_WINDOWS_VERSION_MINOR.'.'.PHP_WINDOWS_VERSION_BUILD
return (function_exists('sapi_windows_vt100_support')
&& @sapi_windows_vt100_support($this->stream))
|| false !== getenv('ANSICON')
|| 'ON' === getenv('ConEmuANSI')
|| 'xterm' === getenv('TERM');
}
return function_exists('posix_isatty') && @posix_isatty($this->stream);
if (function_exists('stream_isatty')) {
return @stream_isatty($this->stream);
}
if (function_exists('posix_isatty')) {
return @posix_isatty($this->stream);
}
$stat = @fstat($this->stream);
// Check if formatted mode is S_IFCHR
return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
}
}

View File

@@ -34,7 +34,7 @@ class ErrorListenerTest extends TestCase
$logger
->expects($this->once())
->method('error')
->with('Error thrown while running command "{command}". Message: "{message}"', array('error' => $error, 'command' => 'test:run --foo=baz buzz', 'message' => 'An error occurred'))
->with('Error thrown while running command "{command}". Message: "{message}"', array('exception' => $error, 'command' => 'test:run --foo=baz buzz', 'message' => 'An error occurred'))
;
$listener = new ErrorListener($logger);
@@ -49,7 +49,7 @@ class ErrorListenerTest extends TestCase
$logger
->expects($this->once())
->method('error')
->with('An error occurred while using the console. Message: "{message}"', array('error' => $error, 'message' => 'An error occurred'))
->with('An error occurred while using the console. Message: "{message}"', array('exception' => $error, 'message' => 'An error occurred'))
;
$listener = new ErrorListener($logger);

View File

@@ -157,6 +157,29 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
$this->assertEquals('AsseticBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
}
public function testAskWithAutocompleteWithExactMatch()
{
if (!$this->hasSttyAvailable()) {
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
}
$inputStream = $this->getInputStream("b\n");
$possibleChoices = array(
'a' => 'berlin',
'b' => 'copenhagen',
'c' => 'amsterdam',
);
$dialog = new QuestionHelper();
$dialog->setHelperSet(new HelperSet(array(new FormatterHelper())));
$question = new ChoiceQuestion('Please select a city', $possibleChoices);
$question->setMaxAttempts(1);
$this->assertSame('b', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
}
public function testAutocompleteWithTrailingBackslash()
{
if (!$this->hasSttyAvailable()) {

View File

@@ -314,6 +314,10 @@ class ArgvInputTest extends TestCase
$input = new ArgvInput(array('cli.php', '-f', 'foo'));
$this->assertTrue($input->hasParameterOption('-f'), '->hasParameterOption() returns true if the given short option is in the raw input');
$input = new ArgvInput(array('cli.php', '-etest'));
$this->assertTrue($input->hasParameterOption('-e'), '->hasParameterOption() returns true if the given short option is in the raw input');
$this->assertFalse($input->hasParameterOption('-s'), '->hasParameterOption() returns true if the given short option is in the raw input');
$input = new ArgvInput(array('cli.php', '--foo', 'foo'));
$this->assertTrue($input->hasParameterOption('--foo'), '->hasParameterOption() returns true if the given short option is in the raw input');
@@ -339,6 +343,46 @@ class ArgvInputTest extends TestCase
$this->assertFalse($input->hasParameterOption('--foo', true), '->hasParameterOption() returns false if the given option is in the raw input but after an end of options signal');
}
public function testHasParameterOptionEdgeCasesAndLimitations()
{
$input = new ArgvInput(array('cli.php', '-fh'));
// hasParameterOption does not know if the previous short option, -f,
// takes a value or not. If -f takes a value, then -fh does NOT include
// -h; Otherwise it does. Since we do not know which short options take
// values, hasParameterOption does not support this use-case.
$this->assertFalse($input->hasParameterOption('-h'), '->hasParameterOption() returns true if the given short option is in the raw input');
// hasParameterOption does detect that `-fh` contains `-f`, since
// `-f` is the first short option in the set.
$this->assertTrue($input->hasParameterOption('-f'), '->hasParameterOption() returns true if the given short option is in the raw input');
// The test below happens to pass, although it might make more sense
// to disallow it, and require the use of
// $input->hasParameterOption('-f') && $input->hasParameterOption('-h')
// instead.
$this->assertTrue($input->hasParameterOption('-fh'), '->hasParameterOption() returns true if the given short option is in the raw input');
// In theory, if -fh is supported, then -hf should also work.
// However, this is not supported.
$this->assertFalse($input->hasParameterOption('-hf'), '->hasParameterOption() returns true if the given short option is in the raw input');
$input = new ArgvInput(array('cli.php', '-f', '-h'));
// If hasParameterOption('-fh') is supported for 'cli.php -fh', then
// one might also expect that it should also be supported for
// 'cli.php -f -h'. However, this is not supported.
$this->assertFalse($input->hasParameterOption('-fh'), '->hasParameterOption() returns true if the given short option is in the raw input');
}
public function testNoWarningOnInvalidParameterOption()
{
$input = new ArgvInput(array('cli.php', '-edev'));
$this->assertTrue($input->hasParameterOption(array('-e', '')));
// No warning thrown
$this->assertFalse($input->hasParameterOption(array('-m', '')));
$this->assertEquals('dev', $input->getParameterOption(array('-e', '')));
// No warning thrown
$this->assertFalse($input->getParameterOption(array('-m', '')));
}
public function testToString()
{
$input = new ArgvInput(array('cli.php', '-f', 'foo'));

View File

@@ -170,5 +170,8 @@ class ArrayInputTest extends TestCase
$input = new ArrayInput(array('-b' => array('bval_1', 'bval_2'), '--f' => array('fval_1', 'fval_2')));
$this->assertSame('-b=bval_1 -b=bval_2 --f=fval_1 --f=fval_2', (string) $input);
$input = new ArrayInput(array('array_arg' => array('val_1', 'val_2')));
$this->assertSame('val_1 val_2', (string) $input);
}
}

View File

@@ -32,7 +32,7 @@
"symfony/event-dispatcher": "",
"symfony/lock": "",
"symfony/process": "",
"psr/log": "For using the console logger"
"psr/log-implementation": "For using the console logger"
},
"conflict": {
"symfony/dependency-injection": "<3.4",

View File

@@ -31,7 +31,7 @@ class CssSelectorConverter
/**
* @param bool $html Whether HTML support should be enabled. Disable it for XML documents
*/
public function __construct($html = true)
public function __construct(bool $html = true)
{
$this->translator = new Translator();

View File

@@ -31,7 +31,7 @@ abstract class AbstractNode implements NodeInterface
/**
* @return string
*/
public function getNodeName()
public function getNodeName(): string
{
if (null === $this->nodeName) {
$this->nodeName = preg_replace('~.*\\\\([^\\\\]+)Node$~', '$1', get_called_class());

View File

@@ -29,14 +29,7 @@ class AttributeNode extends AbstractNode
private $operator;
private $value;
/**
* @param NodeInterface $selector
* @param string $namespace
* @param string $attribute
* @param string $operator
* @param string $value
*/
public function __construct(NodeInterface $selector, $namespace, $attribute, $operator, $value)
public function __construct(NodeInterface $selector, ?string $namespace, string $attribute, string $operator, ?string $value)
{
$this->selector = $selector;
$this->namespace = $namespace;
@@ -45,42 +38,27 @@ class AttributeNode extends AbstractNode
$this->value = $value;
}
/**
* @return NodeInterface
*/
public function getSelector()
public function getSelector(): NodeInterface
{
return $this->selector;
}
/**
* @return string
*/
public function getNamespace()
public function getNamespace(): ?string
{
return $this->namespace;
}
/**
* @return string
*/
public function getAttribute()
public function getAttribute(): string
{
return $this->attribute;
}
/**
* @return string
*/
public function getOperator()
public function getOperator(): string
{
return $this->operator;
}
/**
* @return string
*/
public function getValue()
public function getValue(): ?string
{
return $this->value;
}
@@ -88,7 +66,7 @@ class AttributeNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function getSpecificity()
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
@@ -96,7 +74,7 @@ class AttributeNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function __toString()
public function __toString(): string
{
$attribute = $this->namespace ? $this->namespace.'|'.$this->attribute : $this->attribute;

View File

@@ -26,28 +26,18 @@ class ClassNode extends AbstractNode
private $selector;
private $name;
/**
* @param NodeInterface $selector
* @param string $name
*/
public function __construct(NodeInterface $selector, $name)
public function __construct(NodeInterface $selector, string $name)
{
$this->selector = $selector;
$this->name = $name;
}
/**
* @return NodeInterface
*/
public function getSelector()
public function getSelector(): NodeInterface
{
return $this->selector;
}
/**
* @return string
*/
public function getName()
public function getName(): string
{
return $this->name;
}
@@ -55,7 +45,7 @@ class ClassNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function getSpecificity()
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
@@ -63,7 +53,7 @@ class ClassNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function __toString()
public function __toString(): string
{
return sprintf('%s[%s.%s]', $this->getNodeName(), $this->selector, $this->name);
}

View File

@@ -27,38 +27,24 @@ class CombinedSelectorNode extends AbstractNode
private $combinator;
private $subSelector;
/**
* @param NodeInterface $selector
* @param string $combinator
* @param NodeInterface $subSelector
*/
public function __construct(NodeInterface $selector, $combinator, NodeInterface $subSelector)
public function __construct(NodeInterface $selector, string $combinator, NodeInterface $subSelector)
{
$this->selector = $selector;
$this->combinator = $combinator;
$this->subSelector = $subSelector;
}
/**
* @return NodeInterface
*/
public function getSelector()
public function getSelector(): NodeInterface
{
return $this->selector;
}
/**
* @return string
*/
public function getCombinator()
public function getCombinator(): string
{
return $this->combinator;
}
/**
* @return NodeInterface
*/
public function getSubSelector()
public function getSubSelector(): NodeInterface
{
return $this->subSelector;
}
@@ -66,7 +52,7 @@ class CombinedSelectorNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function getSpecificity()
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
}
@@ -74,7 +60,7 @@ class CombinedSelectorNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function __toString()
public function __toString(): string
{
$combinator = ' ' === $this->combinator ? '<followed>' : $this->combinator;

View File

@@ -30,7 +30,7 @@ class ElementNode extends AbstractNode
* @param string|null $namespace
* @param string|null $element
*/
public function __construct($namespace = null, $element = null)
public function __construct(string $namespace = null, string $element = null)
{
$this->namespace = $namespace;
$this->element = $element;
@@ -55,7 +55,7 @@ class ElementNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function getSpecificity()
public function getSpecificity(): Specificity
{
return new Specificity(0, 0, $this->element ? 1 : 0);
}
@@ -63,7 +63,7 @@ class ElementNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function __toString()
public function __toString(): string
{
$element = $this->element ?: '*';

View File

@@ -34,25 +34,19 @@ class FunctionNode extends AbstractNode
* @param string $name
* @param Token[] $arguments
*/
public function __construct(NodeInterface $selector, $name, array $arguments = array())
public function __construct(NodeInterface $selector, string $name, array $arguments = array())
{
$this->selector = $selector;
$this->name = strtolower($name);
$this->arguments = $arguments;
}
/**
* @return NodeInterface
*/
public function getSelector()
public function getSelector(): NodeInterface
{
return $this->selector;
}
/**
* @return string
*/
public function getName()
public function getName(): string
{
return $this->name;
}
@@ -68,7 +62,7 @@ class FunctionNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function getSpecificity()
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
@@ -76,7 +70,7 @@ class FunctionNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function __toString()
public function __toString(): string
{
$arguments = implode(', ', array_map(function (Token $token) {
return "'".$token->getValue()."'";

View File

@@ -26,28 +26,18 @@ class HashNode extends AbstractNode
private $selector;
private $id;
/**
* @param NodeInterface $selector
* @param string $id
*/
public function __construct(NodeInterface $selector, $id)
public function __construct(NodeInterface $selector, string $id)
{
$this->selector = $selector;
$this->id = $id;
}
/**
* @return NodeInterface
*/
public function getSelector()
public function getSelector(): NodeInterface
{
return $this->selector;
}
/**
* @return string
*/
public function getId()
public function getId(): string
{
return $this->id;
}
@@ -55,7 +45,7 @@ class HashNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function getSpecificity()
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(1, 0, 0));
}
@@ -63,7 +53,7 @@ class HashNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function __toString()
public function __toString(): string
{
return sprintf('%s[%s#%s]', $this->getNodeName(), $this->selector, $this->id);
}

View File

@@ -51,7 +51,7 @@ class NegationNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function getSpecificity()
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
}
@@ -59,7 +59,7 @@ class NegationNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function __toString()
public function __toString(): string
{
return sprintf('%s[%s:not(%s)]', $this->getNodeName(), $this->selector, $this->subSelector);
}

View File

@@ -23,24 +23,9 @@ namespace Symfony\Component\CssSelector\Node;
*/
interface NodeInterface
{
/**
* Returns node's name.
*
* @return string
*/
public function getNodeName();
public function getNodeName(): string;
/**
* Returns node's specificity.
*
* @return Specificity
*/
public function getSpecificity();
public function getSpecificity(): Specificity;
/**
* Returns node's string representation.
*
* @return string
*/
public function __toString();
public function __toString(): string;
}

View File

@@ -26,28 +26,18 @@ class PseudoNode extends AbstractNode
private $selector;
private $identifier;
/**
* @param NodeInterface $selector
* @param string $identifier
*/
public function __construct(NodeInterface $selector, $identifier)
public function __construct(NodeInterface $selector, string $identifier)
{
$this->selector = $selector;
$this->identifier = strtolower($identifier);
}
/**
* @return NodeInterface
*/
public function getSelector()
public function getSelector(): NodeInterface
{
return $this->selector;
}
/**
* @return string
*/
public function getIdentifier()
public function getIdentifier(): string
{
return $this->identifier;
}
@@ -55,7 +45,7 @@ class PseudoNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function getSpecificity()
public function getSpecificity(): Specificity
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
@@ -63,7 +53,7 @@ class PseudoNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function __toString()
public function __toString(): string
{
return sprintf('%s[%s:%s]', $this->getNodeName(), $this->selector, $this->identifier);
}

View File

@@ -26,28 +26,18 @@ class SelectorNode extends AbstractNode
private $tree;
private $pseudoElement;
/**
* @param NodeInterface $tree
* @param null|string $pseudoElement
*/
public function __construct(NodeInterface $tree, $pseudoElement = null)
public function __construct(NodeInterface $tree, string $pseudoElement = null)
{
$this->tree = $tree;
$this->pseudoElement = $pseudoElement ? strtolower($pseudoElement) : null;
}
/**
* @return NodeInterface
*/
public function getTree()
public function getTree(): NodeInterface
{
return $this->tree;
}
/**
* @return null|string
*/
public function getPseudoElement()
public function getPseudoElement(): ?string
{
return $this->pseudoElement;
}
@@ -55,7 +45,7 @@ class SelectorNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function getSpecificity()
public function getSpecificity(): Specificity
{
return $this->tree->getSpecificity()->plus(new Specificity(0, 0, $this->pseudoElement ? 1 : 0));
}
@@ -63,7 +53,7 @@ class SelectorNode extends AbstractNode
/**
* {@inheritdoc}
*/
public function __toString()
public function __toString(): string
{
return sprintf('%s[%s%s]', $this->getNodeName(), $this->tree, $this->pseudoElement ? '::'.$this->pseudoElement : '');
}

View File

@@ -33,32 +33,19 @@ class Specificity
private $b;
private $c;
/**
* @param int $a
* @param int $b
* @param int $c
*/
public function __construct($a, $b, $c)
public function __construct(int $a, int $b, int $c)
{
$this->a = $a;
$this->b = $b;
$this->c = $c;
}
/**
* @return self
*/
public function plus(Specificity $specificity)
public function plus(self $specificity): self
{
return new self($this->a + $specificity->a, $this->b + $specificity->b, $this->c + $specificity->c);
}
/**
* Returns global specificity value.
*
* @return int
*/
public function getValue()
public function getValue(): int
{
return $this->a * self::A_FACTOR + $this->b * self::B_FACTOR + $this->c * self::C_FACTOR;
}
@@ -69,7 +56,7 @@ class Specificity
*
* @return int
*/
public function compareTo(Specificity $specificity)
public function compareTo(self $specificity)
{
if ($this->a !== $specificity->a) {
return $this->a > $specificity->a ? 1 : -1;

View File

@@ -29,7 +29,7 @@ class CommentHandler implements HandlerInterface
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
public function handle(Reader $reader, TokenStream $stream): bool
{
if ('/*' !== $reader->getSubstring(2)) {
return false;

View File

@@ -29,5 +29,5 @@ interface HandlerInterface
/**
* @return bool
*/
public function handle(Reader $reader, TokenStream $stream);
public function handle(Reader $reader, TokenStream $stream): bool;
}

View File

@@ -41,7 +41,7 @@ class HashHandler implements HandlerInterface
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern($this->patterns->getHashPattern());

View File

@@ -41,7 +41,7 @@ class IdentifierHandler implements HandlerInterface
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern($this->patterns->getIdentifierPattern());

View File

@@ -38,7 +38,7 @@ class NumberHandler implements HandlerInterface
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern($this->patterns->getNumberPattern());

View File

@@ -43,7 +43,7 @@ class StringHandler implements HandlerInterface
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
public function handle(Reader $reader, TokenStream $stream): bool
{
$quote = $reader->getSubstring(1);

View File

@@ -30,7 +30,7 @@ class WhitespaceHandler implements HandlerInterface
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
public function handle(Reader $reader, TokenStream $stream): bool
{
$match = $reader->findPattern('~^[ \t\r\n\f]+~');

View File

@@ -37,7 +37,7 @@ class Parser implements ParserInterface
/**
* {@inheritdoc}
*/
public function parse($source)
public function parse(string $source): array
{
$reader = new Reader($source);
$stream = $this->tokenizer->tokenize($reader);
@@ -50,11 +50,9 @@ class Parser implements ParserInterface
*
* @param Token[] $tokens
*
* @return array
*
* @throws SyntaxErrorException
*/
public static function parseSeries(array $tokens)
public static function parseSeries(array $tokens): array
{
foreach ($tokens as $token) {
if ($token->isString()) {
@@ -94,12 +92,7 @@ class Parser implements ParserInterface
);
}
/**
* Parses selector nodes.
*
* @return array
*/
private function parseSelectorList(TokenStream $stream)
private function parseSelectorList(TokenStream $stream): array
{
$stream->skipWhitespace();
$selectors = array();
@@ -118,14 +111,7 @@ class Parser implements ParserInterface
return $selectors;
}
/**
* Parses next selector or combined node.
*
* @return Node\SelectorNode
*
* @throws SyntaxErrorException
*/
private function parserSelectorNode(TokenStream $stream)
private function parserSelectorNode(TokenStream $stream): Node\SelectorNode
{
list($result, $pseudoElement) = $this->parseSimpleSelector($stream);
@@ -158,14 +144,9 @@ class Parser implements ParserInterface
/**
* Parses next simple node (hash, class, pseudo, negation).
*
* @param TokenStream $stream
* @param bool $insideNegation
*
* @return array
*
* @throws SyntaxErrorException
*/
private function parseSimpleSelector(TokenStream $stream, $insideNegation = false)
private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = false): array
{
$stream->skipWhitespace();
@@ -279,12 +260,7 @@ class Parser implements ParserInterface
return array($result, $pseudoElement);
}
/**
* Parses next element node.
*
* @return Node\ElementNode
*/
private function parseElementNode(TokenStream $stream)
private function parseElementNode(TokenStream $stream): Node\ElementNode
{
$peek = $stream->getPeek();
@@ -310,14 +286,7 @@ class Parser implements ParserInterface
return new Node\ElementNode($namespace, $element);
}
/**
* Parses next attribute node.
*
* @return Node\AttributeNode
*
* @throws SyntaxErrorException
*/
private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream)
private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream): Node\AttributeNode
{
$stream->skipWhitespace();
$attribute = $stream->getNextIdentifierOrStar();

View File

@@ -28,9 +28,7 @@ interface ParserInterface
/**
* Parses given selector source into an array of tokens.
*
* @param string $source
*
* @return SelectorNode[]
*/
public function parse($source);
public function parse(string $source): array;
}

View File

@@ -27,56 +27,33 @@ class Reader
private $length;
private $position = 0;
/**
* @param string $source
*/
public function __construct($source)
public function __construct(string $source)
{
$this->source = $source;
$this->length = strlen($source);
}
/**
* @return bool
*/
public function isEOF()
public function isEOF(): bool
{
return $this->position >= $this->length;
}
/**
* @return int
*/
public function getPosition()
public function getPosition(): int
{
return $this->position;
}
/**
* @return int
*/
public function getRemainingLength()
public function getRemainingLength(): int
{
return $this->length - $this->position;
}
/**
* @param int $length
* @param int $offset
*
* @return string
*/
public function getSubstring($length, $offset = 0)
public function getSubstring(int $length, int $offset = 0): string
{
return substr($this->source, $this->position + $offset, $length);
}
/**
* @param string $string
*
* @return int
*/
public function getOffset($string)
public function getOffset(string $string)
{
$position = strpos($this->source, $string, $this->position);
@@ -84,11 +61,9 @@ class Reader
}
/**
* @param string $pattern
*
* @return array|false
*/
public function findPattern($pattern)
public function findPattern(string $pattern)
{
$source = substr($this->source, $this->position);
@@ -99,10 +74,7 @@ class Reader
return false;
}
/**
* @param int $length
*/
public function moveForward($length)
public function moveForward(int $length)
{
$this->position += $length;
}

View File

@@ -31,7 +31,7 @@ class ClassParser implements ParserInterface
/**
* {@inheritdoc}
*/
public function parse($source)
public function parse(string $source): array
{
// Matches an optional namespace, optional element, and required class
// $source = 'test|input.ab6bd_field';

View File

@@ -30,7 +30,7 @@ class ElementParser implements ParserInterface
/**
* {@inheritdoc}
*/
public function parse($source)
public function parse(string $source): array
{
// Matches an optional namespace, required element or `*`
// $source = 'testns|testel';

View File

@@ -34,7 +34,7 @@ class EmptyStringParser implements ParserInterface
/**
* {@inheritdoc}
*/
public function parse($source)
public function parse(string $source): array
{
// Matches an empty string
if ('' == $source) {

View File

@@ -31,7 +31,7 @@ class HashParser implements ParserInterface
/**
* {@inheritdoc}
*/
public function parse($source)
public function parse(string $source): array
{
// Matches an optional namespace, optional element, and required id
// $source = 'test|input#ab6bd_field';

View File

@@ -35,54 +35,34 @@ class Token
private $value;
private $position;
/**
* @param int $type
* @param string $value
* @param int $position
*/
public function __construct($type, $value, $position)
public function __construct(?string $type, ?string $value, ?int $position)
{
$this->type = $type;
$this->value = $value;
$this->position = $position;
}
/**
* @return int
*/
public function getType()
public function getType(): ?int
{
return $this->type;
}
/**
* @return string
*/
public function getValue()
public function getValue(): ?string
{
return $this->value;
}
/**
* @return int
*/
public function getPosition()
public function getPosition(): ?int
{
return $this->position;
}
/**
* @return bool
*/
public function isFileEnd()
public function isFileEnd(): bool
{
return self::TYPE_FILE_END === $this->type;
}
/**
* @return bool
*/
public function isDelimiter(array $values = array())
public function isDelimiter(array $values = array()): bool
{
if (self::TYPE_DELIMITER !== $this->type) {
return false;
@@ -95,50 +75,32 @@ class Token
return in_array($this->value, $values);
}
/**
* @return bool
*/
public function isWhitespace()
public function isWhitespace(): bool
{
return self::TYPE_WHITESPACE === $this->type;
}
/**
* @return bool
*/
public function isIdentifier()
public function isIdentifier(): bool
{
return self::TYPE_IDENTIFIER === $this->type;
}
/**
* @return bool
*/
public function isHash()
public function isHash(): bool
{
return self::TYPE_HASH === $this->type;
}
/**
* @return bool
*/
public function isNumber()
public function isNumber(): bool
{
return self::TYPE_NUMBER === $this->type;
}
/**
* @return bool
*/
public function isString()
public function isString(): bool
{
return self::TYPE_STRING === $this->type;
}
/**
* @return string
*/
public function __toString()
public function __toString(): string
{
if ($this->value) {
return sprintf('<%s "%s" at %s>', $this->type, $this->value, $this->position);

View File

@@ -30,36 +30,21 @@ class TokenizerEscaping
$this->patterns = $patterns;
}
/**
* @param string $value
*
* @return string
*/
public function escapeUnicode($value)
public function escapeUnicode(string $value): string
{
$value = $this->replaceUnicodeSequences($value);
return preg_replace($this->patterns->getSimpleEscapePattern(), '$1', $value);
}
/**
* @param string $value
*
* @return string
*/
public function escapeUnicodeAndNewLine($value)
public function escapeUnicodeAndNewLine(string $value): string
{
$value = preg_replace($this->patterns->getNewLineEscapePattern(), '', $value);
return $this->escapeUnicode($value);
}
/**
* @param string $value
*
* @return string
*/
private function replaceUnicodeSequences($value)
private function replaceUnicodeSequences(string $value): string
{
return preg_replace_callback($this->patterns->getUnicodeEscapePattern(), function ($match) {
$c = hexdec($match[1]);

View File

@@ -46,66 +46,43 @@ class TokenizerPatterns
$this->nonAsciiPattern = '[^\x00-\x7F]';
$this->nmCharPattern = '[_a-z0-9-]|'.$this->escapePattern.'|'.$this->nonAsciiPattern;
$this->nmStartPattern = '[_a-z]|'.$this->escapePattern.'|'.$this->nonAsciiPattern;
$this->identifierPattern = '(?:'.$this->nmStartPattern.')(?:'.$this->nmCharPattern.')*';
$this->identifierPattern = '-?(?:'.$this->nmStartPattern.')(?:'.$this->nmCharPattern.')*';
$this->hashPattern = '#((?:'.$this->nmCharPattern.')+)';
$this->numberPattern = '[+-]?(?:[0-9]*\.[0-9]+|[0-9]+)';
$this->quotedStringPattern = '([^\n\r\f%s]|'.$this->stringEscapePattern.')*';
}
/**
* @return string
*/
public function getNewLineEscapePattern()
public function getNewLineEscapePattern(): string
{
return '~^'.$this->newLineEscapePattern.'~';
}
/**
* @return string
*/
public function getSimpleEscapePattern()
public function getSimpleEscapePattern(): string
{
return '~^'.$this->simpleEscapePattern.'~';
}
/**
* @return string
*/
public function getUnicodeEscapePattern()
public function getUnicodeEscapePattern(): string
{
return '~^'.$this->unicodeEscapePattern.'~i';
}
/**
* @return string
*/
public function getIdentifierPattern()
public function getIdentifierPattern(): string
{
return '~^'.$this->identifierPattern.'~i';
}
/**
* @return string
*/
public function getHashPattern()
public function getHashPattern(): string
{
return '~^'.$this->hashPattern.'~i';
}
/**
* @return string
*/
public function getNumberPattern()
public function getNumberPattern(): string
{
return '~^'.$this->numberPattern.'~';
}
/**
* @param string $quote
*
* @return string
*/
public function getQuotedStringPattern($quote)
public function getQuotedStringPattern(string $quote): string
{
return '~^'.sprintf($this->quotedStringPattern, $quote).'~i';
}

View File

@@ -59,7 +59,7 @@ class CssSelectorConverterTest extends TestCase
array('h1', 'h1'),
array('foo|h1', 'foo:h1'),
array('h1, h2, h3', 'h1 | h2 | h3'),
array('h1:nth-child(3n+1)', "*/*[name() = 'h1' and (position() - 1 >= 0 and (position() - 1) mod 3 = 0)]"),
array('h1:nth-child(3n+1)', "*/*[(name() = 'h1') and (position() - 1 >= 0 and (position() - 1) mod 3 = 0)]"),
array('h1 > p', 'h1/p'),
array('h1#foo', "h1[@id = 'foo']"),
array('h1.foo', "h1[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"),

View File

@@ -186,6 +186,7 @@ class ParserTest extends TestCase
array('foo:after', 'Element[foo]', 'after'),
array('foo::selection', 'Element[foo]', 'selection'),
array('lorem#ipsum ~ a#b.c[href]:empty::selection', 'CombinedSelector[Hash[Element[lorem]#ipsum] ~ Pseudo[Attribute[Class[Hash[Element[a]#b].c][href]]:empty]]', 'selection'),
array('video::-webkit-media-controls', 'Element[video]', '-webkit-media-controls'),
);
}

View File

@@ -102,18 +102,20 @@ class TranslatorTest extends TestCase
array('e[foo^="bar"]', "e[@foo and starts-with(@foo, 'bar')]"),
array('e[foo$="bar"]', "e[@foo and substring(@foo, string-length(@foo)-2) = 'bar']"),
array('e[foo*="bar"]', "e[@foo and contains(@foo, 'bar')]"),
array('e[foo!="bar"]', "e[not(@foo) or @foo != 'bar']"),
array('e[foo!="bar"][foo!="baz"]', "e[(not(@foo) or @foo != 'bar') and (not(@foo) or @foo != 'baz')]"),
array('e[hreflang|="en"]', "e[@hreflang and (@hreflang = 'en' or starts-with(@hreflang, 'en-'))]"),
array('e:nth-child(1)', "*/*[name() = 'e' and (position() = 1)]"),
array('e:nth-last-child(1)', "*/*[name() = 'e' and (position() = last() - 0)]"),
array('e:nth-last-child(2n+2)', "*/*[name() = 'e' and (last() - position() - 1 >= 0 and (last() - position() - 1) mod 2 = 0)]"),
array('e:nth-child(1)', "*/*[(name() = 'e') and (position() = 1)]"),
array('e:nth-last-child(1)', "*/*[(name() = 'e') and (position() = last() - 0)]"),
array('e:nth-last-child(2n+2)', "*/*[(name() = 'e') and (last() - position() - 1 >= 0 and (last() - position() - 1) mod 2 = 0)]"),
array('e:nth-of-type(1)', '*/e[position() = 1]'),
array('e:nth-last-of-type(1)', '*/e[position() = last() - 0]'),
array('div e:nth-last-of-type(1) .aclass', "div/descendant-or-self::*/e[position() = last() - 0]/descendant-or-self::*/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' aclass ')]"),
array('e:first-child', "*/*[name() = 'e' and (position() = 1)]"),
array('e:last-child', "*/*[name() = 'e' and (position() = last())]"),
array('e:first-child', "*/*[(name() = 'e') and (position() = 1)]"),
array('e:last-child', "*/*[(name() = 'e') and (position() = last())]"),
array('e:first-of-type', '*/e[position() = 1]'),
array('e:last-of-type', '*/e[position() = last()]'),
array('e:only-child', "*/*[name() = 'e' and (last() = 1)]"),
array('e:only-child', "*/*[(name() = 'e') and (last() = 1)]"),
array('e:only-of-type', 'e[last() = 1]'),
array('e:empty', 'e[not(*) and not(string-length())]'),
array('e:EmPTY', 'e[not(*) and not(string-length())]'),
@@ -127,7 +129,7 @@ class TranslatorTest extends TestCase
array('e:nOT(*)', 'e[0]'),
array('e f', 'e/descendant-or-self::*/f'),
array('e > f', 'e/f'),
array('e + f', "e/following-sibling::*[name() = 'f' and (position() = 1)]"),
array('e + f', "e/following-sibling::*[(name() = 'f') and (position() = 1)]"),
array('e ~ f', 'e/following-sibling::f'),
array('div#container p', "div[@id = 'container']/descendant-or-self::*/p"),
);

View File

@@ -43,38 +43,17 @@ class AttributeMatchingExtension extends AbstractExtension
);
}
/**
* @param XPathExpr $xpath
* @param string $attribute
* @param string $value
*
* @return XPathExpr
*/
public function translateExists(XPathExpr $xpath, $attribute, $value)
public function translateExists(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($attribute);
}
/**
* @param XPathExpr $xpath
* @param string $attribute
* @param string $value
*
* @return XPathExpr
*/
public function translateEquals(XPathExpr $xpath, $attribute, $value)
public function translateEquals(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition(sprintf('%s = %s', $attribute, Translator::getXpathLiteral($value)));
}
/**
* @param XPathExpr $xpath
* @param string $attribute
* @param string $value
*
* @return XPathExpr
*/
public function translateIncludes(XPathExpr $xpath, $attribute, $value)
public function translateIncludes(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? sprintf(
'%1$s and contains(concat(\' \', normalize-space(%1$s), \' \'), %2$s)',
@@ -83,14 +62,7 @@ class AttributeMatchingExtension extends AbstractExtension
) : '0');
}
/**
* @param XPathExpr $xpath
* @param string $attribute
* @param string $value
*
* @return XPathExpr
*/
public function translateDashMatch(XPathExpr $xpath, $attribute, $value)
public function translateDashMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition(sprintf(
'%1$s and (%1$s = %2$s or starts-with(%1$s, %3$s))',
@@ -100,14 +72,7 @@ class AttributeMatchingExtension extends AbstractExtension
));
}
/**
* @param XPathExpr $xpath
* @param string $attribute
* @param string $value
*
* @return XPathExpr
*/
public function translatePrefixMatch(XPathExpr $xpath, $attribute, $value)
public function translatePrefixMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? sprintf(
'%1$s and starts-with(%1$s, %2$s)',
@@ -116,14 +81,7 @@ class AttributeMatchingExtension extends AbstractExtension
) : '0');
}
/**
* @param XPathExpr $xpath
* @param string $attribute
* @param string $value
*
* @return XPathExpr
*/
public function translateSuffixMatch(XPathExpr $xpath, $attribute, $value)
public function translateSuffixMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? sprintf(
'%1$s and substring(%1$s, string-length(%1$s)-%2$s) = %3$s',
@@ -133,14 +91,7 @@ class AttributeMatchingExtension extends AbstractExtension
) : '0');
}
/**
* @param XPathExpr $xpath
* @param string $attribute
* @param string $value
*
* @return XPathExpr
*/
public function translateSubstringMatch(XPathExpr $xpath, $attribute, $value)
public function translateSubstringMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition($value ? sprintf(
'%1$s and contains(%1$s, %2$s)',
@@ -149,14 +100,7 @@ class AttributeMatchingExtension extends AbstractExtension
) : '0');
}
/**
* @param XPathExpr $xpath
* @param string $attribute
* @param string $value
*
* @return XPathExpr
*/
public function translateDifferent(XPathExpr $xpath, $attribute, $value)
public function translateDifferent(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr
{
return $xpath->addCondition(sprintf(
$value ? 'not(%1$s) or %1$s != %2$s' : '%s != %s',

View File

@@ -28,7 +28,7 @@ class CombinationExtension extends AbstractExtension
/**
* {@inheritdoc}
*/
public function getCombinationTranslators()
public function getCombinationTranslators(): array
{
return array(
' ' => array($this, 'translateDescendant'),
@@ -41,7 +41,7 @@ class CombinationExtension extends AbstractExtension
/**
* @return XPathExpr
*/
public function translateDescendant(XPathExpr $xpath, XPathExpr $combinedXpath)
public function translateDescendant(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr
{
return $xpath->join('/descendant-or-self::*/', $combinedXpath);
}

View File

@@ -46,16 +46,9 @@ class FunctionExtension extends AbstractExtension
}
/**
* @param XPathExpr $xpath
* @param FunctionNode $function
* @param bool $last
* @param bool $addNameTest
*
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function translateNthChild(XPathExpr $xpath, FunctionNode $function, $last = false, $addNameTest = true)
public function translateNthChild(XPathExpr $xpath, FunctionNode $function, bool $last = false, bool $addNameTest = true): XPathExpr
{
try {
list($a, $b) = Parser::parseSeries($function->getArguments());
@@ -110,28 +103,20 @@ class FunctionExtension extends AbstractExtension
// -1n+6 means elements 6 and previous
}
/**
* @return XPathExpr
*/
public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function)
public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
return $this->translateNthChild($xpath, $function, true);
}
/**
* @return XPathExpr
*/
public function translateNthOfType(XPathExpr $xpath, FunctionNode $function)
public function translateNthOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
return $this->translateNthChild($xpath, $function, false, false);
}
/**
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function)
public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
if ('*' === $xpath->getElement()) {
throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.');
@@ -141,11 +126,9 @@ class FunctionExtension extends AbstractExtension
}
/**
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function translateContains(XPathExpr $xpath, FunctionNode $function)
public function translateContains(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
$arguments = $function->getArguments();
foreach ($arguments as $token) {
@@ -164,11 +147,9 @@ class FunctionExtension extends AbstractExtension
}
/**
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function translateLang(XPathExpr $xpath, FunctionNode $function)
public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
$arguments = $function->getArguments();
foreach ($arguments as $token) {

View File

@@ -33,21 +33,15 @@ class NodeExtension extends AbstractExtension
private $flags;
/**
* @param int $flags
*/
public function __construct($flags = 0)
public function __construct(int $flags = 0)
{
$this->flags = $flags;
}
/**
* @param int $flag
* @param bool $on
*
* @return $this
*/
public function setFlag($flag, $on)
public function setFlag(int $flag, bool $on)
{
if ($on && !$this->hasFlag($flag)) {
$this->flags += $flag;
@@ -60,12 +54,7 @@ class NodeExtension extends AbstractExtension
return $this;
}
/**
* @param int $flag
*
* @return bool
*/
public function hasFlag($flag)
public function hasFlag(int $flag): bool
{
return (bool) ($this->flags & $flag);
}
@@ -88,26 +77,17 @@ class NodeExtension extends AbstractExtension
);
}
/**
* @return XPathExpr
*/
public function translateSelector(Node\SelectorNode $node, Translator $translator)
public function translateSelector(Node\SelectorNode $node, Translator $translator): XPathExpr
{
return $translator->nodeToXPath($node->getTree());
}
/**
* @return XPathExpr
*/
public function translateCombinedSelector(Node\CombinedSelectorNode $node, Translator $translator)
public function translateCombinedSelector(Node\CombinedSelectorNode $node, Translator $translator): XPathExpr
{
return $translator->addCombination($node->getCombinator(), $node->getSelector(), $node->getSubSelector());
}
/**
* @return XPathExpr
*/
public function translateNegation(Node\NegationNode $node, Translator $translator)
public function translateNegation(Node\NegationNode $node, Translator $translator): XPathExpr
{
$xpath = $translator->nodeToXPath($node->getSelector());
$subXpath = $translator->nodeToXPath($node->getSubSelector());
@@ -120,30 +100,21 @@ class NodeExtension extends AbstractExtension
return $xpath->addCondition('0');
}
/**
* @return XPathExpr
*/
public function translateFunction(Node\FunctionNode $node, Translator $translator)
public function translateFunction(Node\FunctionNode $node, Translator $translator): XPathExpr
{
$xpath = $translator->nodeToXPath($node->getSelector());
return $translator->addFunction($xpath, $node);
}
/**
* @return XPathExpr
*/
public function translatePseudo(Node\PseudoNode $node, Translator $translator)
public function translatePseudo(Node\PseudoNode $node, Translator $translator): XPathExpr
{
$xpath = $translator->nodeToXPath($node->getSelector());
return $translator->addPseudoClass($xpath, $node->getIdentifier());
}
/**
* @return XPathExpr
*/
public function translateAttribute(Node\AttributeNode $node, Translator $translator)
public function translateAttribute(Node\AttributeNode $node, Translator $translator): XPathExpr
{
$name = $node->getAttribute();
$safe = $this->isSafeName($name);
@@ -168,30 +139,21 @@ class NodeExtension extends AbstractExtension
return $translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value);
}
/**
* @return XPathExpr
*/
public function translateClass(Node\ClassNode $node, Translator $translator)
public function translateClass(Node\ClassNode $node, Translator $translator): XPathExpr
{
$xpath = $translator->nodeToXPath($node->getSelector());
return $translator->addAttributeMatching($xpath, '~=', '@class', $node->getName());
}
/**
* @return XPathExpr
*/
public function translateHash(Node\HashNode $node, Translator $translator)
public function translateHash(Node\HashNode $node, Translator $translator): XPathExpr
{
$xpath = $translator->nodeToXPath($node->getSelector());
return $translator->addAttributeMatching($xpath, '=', '@id', $node->getId());
}
/**
* @return XPathExpr
*/
public function translateElement(Node\ElementNode $node)
public function translateElement(Node\ElementNode $node): XPathExpr
{
$element = $node->getElement();
@@ -228,14 +190,7 @@ class NodeExtension extends AbstractExtension
return 'node';
}
/**
* Tests if given name is safe.
*
* @param string $name
*
* @return bool
*/
private function isSafeName($name)
private function isSafeName(string $name): bool
{
return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name);
}

View File

@@ -61,12 +61,7 @@ class Translator implements TranslatorInterface
;
}
/**
* @param string $element
*
* @return string
*/
public static function getXpathLiteral($element)
public static function getXpathLiteral(string $element): string
{
if (false === strpos($element, "'")) {
return "'".$element."'";
@@ -95,7 +90,7 @@ class Translator implements TranslatorInterface
/**
* {@inheritdoc}
*/
public function cssToXPath($cssExpr, $prefix = 'descendant-or-self::')
public function cssToXPath(string $cssExpr, string $prefix = 'descendant-or-self::'): string
{
$selectors = $this->parseSelectors($cssExpr);
@@ -114,17 +109,12 @@ class Translator implements TranslatorInterface
/**
* {@inheritdoc}
*/
public function selectorToXPath(SelectorNode $selector, $prefix = 'descendant-or-self::')
public function selectorToXPath(SelectorNode $selector, string $prefix = 'descendant-or-self::'): string
{
return ($prefix ?: '').$this->nodeToXPath($selector);
}
/**
* Registers an extension.
*
* @return $this
*/
public function registerExtension(Extension\ExtensionInterface $extension)
public function registerExtension(Extension\ExtensionInterface $extension): self
{
$this->extensions[$extension->getName()] = $extension;
@@ -138,13 +128,9 @@ class Translator implements TranslatorInterface
}
/**
* @param string $name
*
* @return Extension\ExtensionInterface
*
* @throws ExpressionErrorException
*/
public function getExtension($name)
public function getExtension(string $name): Extension\ExtensionInterface
{
if (!isset($this->extensions[$name])) {
throw new ExpressionErrorException(sprintf('Extension "%s" not registered.', $name));
@@ -153,12 +139,7 @@ class Translator implements TranslatorInterface
return $this->extensions[$name];
}
/**
* Registers a shortcut parser.
*
* @return $this
*/
public function registerParserShortcut(ParserInterface $shortcut)
public function registerParserShortcut(ParserInterface $shortcut): self
{
$this->shortcutParsers[] = $shortcut;
@@ -166,11 +147,9 @@ class Translator implements TranslatorInterface
}
/**
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function nodeToXPath(NodeInterface $node)
public function nodeToXPath(NodeInterface $node): XPathExpr
{
if (!isset($this->nodeTranslators[$node->getNodeName()])) {
throw new ExpressionErrorException(sprintf('Node "%s" not supported.', $node->getNodeName()));
@@ -180,15 +159,9 @@ class Translator implements TranslatorInterface
}
/**
* @param string $combiner
* @param NodeInterface $xpath
* @param NodeInterface $combinedXpath
*
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function addCombination($combiner, NodeInterface $xpath, NodeInterface $combinedXpath)
public function addCombination(string $combiner, NodeInterface $xpath, NodeInterface $combinedXpath): XPathExpr
{
if (!isset($this->combinationTranslators[$combiner])) {
throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner));
@@ -198,11 +171,9 @@ class Translator implements TranslatorInterface
}
/**
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function addFunction(XPathExpr $xpath, FunctionNode $function)
public function addFunction(XPathExpr $xpath, FunctionNode $function): XPathExpr
{
if (!isset($this->functionTranslators[$function->getName()])) {
throw new ExpressionErrorException(sprintf('Function "%s" not supported.', $function->getName()));
@@ -212,14 +183,9 @@ class Translator implements TranslatorInterface
}
/**
* @param XPathExpr $xpath
* @param string $pseudoClass
*
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function addPseudoClass(XPathExpr $xpath, $pseudoClass)
public function addPseudoClass(XPathExpr $xpath, string $pseudoClass): XPathExpr
{
if (!isset($this->pseudoClassTranslators[$pseudoClass])) {
throw new ExpressionErrorException(sprintf('Pseudo-class "%s" not supported.', $pseudoClass));
@@ -229,16 +195,9 @@ class Translator implements TranslatorInterface
}
/**
* @param XPathExpr $xpath
* @param string $operator
* @param string $attribute
* @param string $value
*
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function addAttributeMatching(XPathExpr $xpath, $operator, $attribute, $value)
public function addAttributeMatching(XPathExpr $xpath, string $operator, string $attribute, $value): XPathExpr
{
if (!isset($this->attributeMatchingTranslators[$operator])) {
throw new ExpressionErrorException(sprintf('Attribute matcher operator "%s" not supported.', $operator));
@@ -248,11 +207,9 @@ class Translator implements TranslatorInterface
}
/**
* @param string $css
*
* @return SelectorNode[]
*/
private function parseSelectors($css)
private function parseSelectors(string $css)
{
foreach ($this->shortcutParsers as $shortcut) {
$tokens = $shortcut->parse($css);

View File

@@ -27,21 +27,11 @@ interface TranslatorInterface
{
/**
* Translates a CSS selector to an XPath expression.
*
* @param string $cssExpr
* @param string $prefix
*
* @return string
*/
public function cssToXPath($cssExpr, $prefix = 'descendant-or-self::');
public function cssToXPath(string $cssExpr, string $prefix = 'descendant-or-self::'): string;
/**
* Translates a parsed selector node to an XPath expression.
*
* @param SelectorNode $selector
* @param string $prefix
*
* @return string
*/
public function selectorToXPath(SelectorNode $selector, $prefix = 'descendant-or-self::');
public function selectorToXPath(SelectorNode $selector, string $prefix = 'descendant-or-self::'): string;
}

View File

@@ -27,13 +27,7 @@ class XPathExpr
private $element;
private $condition;
/**
* @param string $path
* @param string $element
* @param string $condition
* @param bool $starPrefix
*/
public function __construct($path = '', $element = '*', $condition = '', $starPrefix = false)
public function __construct(string $path = '', string $element = '*', string $condition = '', bool $starPrefix = false)
{
$this->path = $path;
$this->element = $element;
@@ -44,38 +38,24 @@ class XPathExpr
}
}
/**
* @return string
*/
public function getElement()
public function getElement(): string
{
return $this->element;
}
/**
* @param $condition
*
* @return $this
*/
public function addCondition($condition)
public function addCondition(string $condition): self
{
$this->condition = $this->condition ? sprintf('%s and (%s)', $this->condition, $condition) : $condition;
$this->condition = $this->condition ? sprintf('(%s) and (%s)', $this->condition, $condition) : $condition;
return $this;
}
/**
* @return string
*/
public function getCondition()
public function getCondition(): string
{
return $this->condition;
}
/**
* @return $this
*/
public function addNameTest()
public function addNameTest(): self
{
if ('*' !== $this->element) {
$this->addCondition('name() = '.Translator::getXpathLiteral($this->element));
@@ -85,10 +65,7 @@ class XPathExpr
return $this;
}
/**
* @return $this
*/
public function addStarPrefix()
public function addStarPrefix(): self
{
$this->path .= '*/';
@@ -98,12 +75,9 @@ class XPathExpr
/**
* Joins another XPathExpr with a combiner.
*
* @param string $combiner
* @param XPathExpr $expr
*
* @return $this
*/
public function join($combiner, XPathExpr $expr)
public function join(string $combiner, self $expr): self
{
$path = $this->__toString().$combiner;
@@ -118,10 +92,7 @@ class XPathExpr
return $this;
}
/**
* @return string
*/
public function __toString()
public function __toString(): string
{
$path = $this->path.$this->element;
$condition = null === $this->condition || '' === $this->condition ? '' : '['.$this->condition.']';

View File

@@ -20,7 +20,7 @@
}
],
"require": {
"php": "^5.5.9|>=7.0.8"
"php": "^7.1.3"
},
"autoload": {
"psr-4": { "Symfony\\Component\\CssSelector\\": "" },
@@ -31,7 +31,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
"dev-master": "4.1-dev"
}
}
}

View File

@@ -45,7 +45,7 @@ class Debug
error_reporting(E_ALL);
}
if ('cli' !== PHP_SAPI) {
if (!\in_array(PHP_SAPI, array('cli', 'phpdbg'), true)) {
ini_set('display_errors', 0);
ExceptionHandler::register();
} elseif ($displayErrors && (!ini_get('log_errors') || ini_get('error_log'))) {

View File

@@ -141,7 +141,7 @@ class DebugClassLoader
if ($this->isFinder && !isset($this->loaded[$class])) {
$this->loaded[$class] = true;
if ($file = $this->classLoader[0]->findFile($class) ?: false) {
$wasCached = \function_exists('opcache_is_script_cached') && opcache_is_script_cached($file);
$wasCached = \function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file);
require $file;

View File

@@ -136,9 +136,20 @@ class ErrorHandler
}
if (!$replace && $prev) {
restore_error_handler();
$handlerIsRegistered = is_array($prev) && $handler === $prev[0];
} else {
$handlerIsRegistered = true;
}
if (is_array($prev = set_exception_handler(array($handler, 'handleException'))) && $prev[0] === $handler) {
if (is_array($prev = set_exception_handler(array($handler, 'handleException'))) && $prev[0] instanceof self) {
restore_exception_handler();
if (!$handlerIsRegistered) {
$handler = $prev[0];
} elseif ($handler !== $prev[0] && $replace) {
set_exception_handler(array($handler, 'handleException'));
$p = $prev[0]->setExceptionHandler(null);
$handler->setExceptionHandler($p);
$prev[0]->setExceptionHandler($p);
}
} else {
$handler->setExceptionHandler($prev);
}
@@ -372,14 +383,16 @@ class ErrorHandler
public function handleError($type, $message, $file, $line)
{
// Level is the current error reporting level to manage silent error.
$level = error_reporting();
$silenced = 0 === ($level & $type);
// Strong errors are not authorized to be silenced.
$level = error_reporting() | E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED;
$level |= E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED;
$log = $this->loggedErrors & $type;
$throw = $this->thrownErrors & $type & $level;
$type &= $level | $this->screamedErrors;
if (!$type || (!$log && !$throw)) {
return $type && $log;
return !$silenced && $type && $log;
}
$scope = $this->scopedErrors & $type;
@@ -513,7 +526,7 @@ class ErrorHandler
}
}
return $type && $log;
return !$silenced && $type && $log;
}
/**
@@ -568,15 +581,16 @@ class ErrorHandler
}
}
}
$exceptionHandler = $this->exceptionHandler;
$this->exceptionHandler = null;
try {
if (null !== $this->exceptionHandler) {
return \call_user_func($this->exceptionHandler, $exception);
if (null !== $exceptionHandler) {
return \call_user_func($exceptionHandler, $exception);
}
$handlerException = $handlerException ?: $exception;
} catch (\Exception $handlerException) {
} catch (\Throwable $handlerException) {
}
$this->exceptionHandler = null;
if ($exception === $handlerException) {
self::$reservedMemory = null; // Disable the fatal error handler
throw $exception; // Give back $exception to the native handler

View File

@@ -36,7 +36,8 @@ class FatalThrowableError extends FatalErrorException
$e->getCode(),
$severity,
$e->getFile(),
$e->getLine()
$e->getLine(),
$e->getPrevious()
);
$this->setTrace($e->getTrace());

View File

@@ -157,7 +157,7 @@ class FlattenException
return $this->previous;
}
public function setPrevious(FlattenException $previous)
public function setPrevious(self $previous)
{
$this->previous = $previous;
}

View File

@@ -40,7 +40,7 @@ class ExceptionHandler
{
$this->debug = $debug;
$this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8';
$this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format');
$this->fileLinkFormat = $fileLinkFormat;
}
/**
@@ -355,13 +355,29 @@ EOF;
private function formatPath($path, $line)
{
$file = $this->escapeHtml(preg_match('#[^/\\\\]*+$#', $path, $file) ? $file[0] : $path);
$fmt = $this->fileLinkFormat;
$fmt = $this->fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format');
if ($fmt && $link = is_string($fmt) ? strtr($fmt, array('%f' => $path, '%l' => $line)) : $fmt->format($path, $line)) {
return sprintf('<span class="block trace-file-path">in <a href="%s" title="Go to source">%s (line %d)</a></span>', $this->escapeHtml($link), $file, $line);
if (!$fmt) {
return sprintf('<span class="block trace-file-path">in <a title="%s%3$s"><strong>%s</strong>%s</a></span>', $this->escapeHtml($path), $file, 0 < $line ? ' line '.$line : '');
}
return sprintf('<span class="block trace-file-path">in <a title="%s line %3$d"><strong>%s</strong> (line %d)</a></span>', $this->escapeHtml($path), $file, $line);
if (\is_string($fmt)) {
$i = strpos($f = $fmt, '&', max(strrpos($f, '%f'), strrpos($f, '%l'))) ?: strlen($f);
$fmt = array(substr($f, 0, $i)) + preg_split('/&([^>]++)>/', substr($f, $i), -1, PREG_SPLIT_DELIM_CAPTURE);
for ($i = 1; isset($fmt[$i]); ++$i) {
if (0 === strpos($path, $k = $fmt[$i++])) {
$path = substr_replace($path, $fmt[$i], 0, strlen($k));
break;
}
}
$link = strtr($fmt[0], array('%f' => $path, '%l' => $line));
} else {
$link = $fmt->format($path, $line);
}
return sprintf('<span class="block trace-file-path">in <a href="%s" title="Go to source"><strong>%s</string>%s</a></span>', $this->escapeHtml($link), $file, 0 < $line ? ' line '.$line : '');
}
/**

View File

@@ -35,7 +35,7 @@ class ErrorHandlerTest extends TestCase
$newHandler = new ErrorHandler();
$this->assertSame($newHandler, ErrorHandler::register($newHandler, false));
$this->assertSame($handler, ErrorHandler::register($newHandler, false));
$h = set_error_handler('var_dump');
restore_error_handler();
$this->assertSame(array($handler, 'handleError'), $h);
@@ -65,6 +65,30 @@ class ErrorHandlerTest extends TestCase
}
}
public function testErrorGetLast()
{
$handler = ErrorHandler::register();
$logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
$handler->setDefaultLogger($logger);
$handler->screamAt(E_ALL);
try {
@trigger_error('Hello', E_USER_WARNING);
$expected = array(
'type' => E_USER_WARNING,
'message' => 'Hello',
'file' => __FILE__,
'line' => __LINE__ - 5,
);
$this->assertSame($expected, error_get_last());
} catch (\Exception $e) {
restore_error_handler();
restore_exception_handler();
throw $e;
}
}
public function testNotice()
{
ErrorHandler::register();
@@ -545,4 +569,18 @@ class ErrorHandlerTest extends TestCase
restore_exception_handler();
}
}
/**
* @expectedException \Exception
* @group no-hhvm
*/
public function testCustomExceptionHandler()
{
$handler = new ErrorHandler();
$handler->setExceptionHandler(function ($e) use ($handler) {
$handler->handleException($e);
});
$handler->handleException(new \Exception());
}
}

View File

@@ -111,7 +111,7 @@ class FlattenExceptionTest extends TestCase
/**
* @dataProvider flattenDataProvider
*/
public function testFlattenHttpException(\Exception $exception, $statusCode)
public function testFlattenHttpException(\Exception $exception)
{
$flattened = FlattenException::create($exception);
$flattened2 = FlattenException::create($exception);
@@ -126,7 +126,7 @@ class FlattenExceptionTest extends TestCase
/**
* @dataProvider flattenDataProvider
*/
public function testPrevious(\Exception $exception, $statusCode)
public function testPrevious(\Exception $exception)
{
$flattened = FlattenException::create($exception);
$flattened2 = FlattenException::create($exception);
@@ -173,7 +173,7 @@ class FlattenExceptionTest extends TestCase
/**
* @dataProvider flattenDataProvider
*/
public function testToArray(\Exception $exception, $statusCode)
public function testToArray(\Exception $exception)
{
$flattened = FlattenException::create($exception);
$flattened->setTrace(array(), 'foo.php', 123);
@@ -193,7 +193,7 @@ class FlattenExceptionTest extends TestCase
public function flattenDataProvider()
{
return array(
array(new \Exception('test', 123), 500),
array(new \Exception('test', 123)),
);
}

View File

@@ -1,6 +1,19 @@
CHANGELOG
=========
4.1.0
-----
* added support for invokable event listeners tagged with `kernel.event_listener` by default
* The `TraceableEventDispatcher::getOrphanedEvents()` method has been added.
* The `TraceableEventDispatcherInterface` has been deprecated.
4.0.0
-----
* removed the `ContainerAwareEventDispatcher` class
* added the `reset()` method to the `TraceableEventDispatcherInterface`
3.4.0
-----

View File

@@ -1,197 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Lazily loads listeners and subscribers from the dependency injection
* container.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Jordan Alliot <jordan.alliot@gmail.com>
*
* @deprecated since 3.3, to be removed in 4.0. Use EventDispatcher with closure factories instead.
*/
class ContainerAwareEventDispatcher extends EventDispatcher
{
private $container;
/**
* The service IDs of the event listeners and subscribers.
*/
private $listenerIds = array();
/**
* The services registered as listeners.
*/
private $listeners = array();
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$class = get_class($this);
if ($this instanceof \PHPUnit_Framework_MockObject_MockObject || $this instanceof \Prophecy\Doubler\DoubleInterface) {
$class = get_parent_class($class);
}
if (__CLASS__ !== $class) {
@trigger_error(sprintf('The %s class is deprecated since Symfony 3.3 and will be removed in 4.0. Use EventDispatcher with closure factories instead.', __CLASS__), E_USER_DEPRECATED);
}
}
/**
* Adds a service as event listener.
*
* @param string $eventName Event for which the listener is added
* @param array $callback The service ID of the listener service & the method
* name that has to be called
* @param int $priority The higher this value, the earlier an event listener
* will be triggered in the chain.
* Defaults to 0.
*
* @throws \InvalidArgumentException
*/
public function addListenerService($eventName, $callback, $priority = 0)
{
@trigger_error(sprintf('The %s class is deprecated since Symfony 3.3 and will be removed in 4.0. Use EventDispatcher with closure factories instead.', __CLASS__), E_USER_DEPRECATED);
if (!is_array($callback) || 2 !== count($callback)) {
throw new \InvalidArgumentException('Expected an array("service", "method") argument');
}
$this->listenerIds[$eventName][] = array($callback[0], $callback[1], $priority);
}
public function removeListener($eventName, $listener)
{
$this->lazyLoad($eventName);
if (isset($this->listenerIds[$eventName])) {
foreach ($this->listenerIds[$eventName] as $i => list($serviceId, $method)) {
$key = $serviceId.'.'.$method;
if (isset($this->listeners[$eventName][$key]) && $listener === array($this->listeners[$eventName][$key], $method)) {
unset($this->listeners[$eventName][$key]);
if (empty($this->listeners[$eventName])) {
unset($this->listeners[$eventName]);
}
unset($this->listenerIds[$eventName][$i]);
if (empty($this->listenerIds[$eventName])) {
unset($this->listenerIds[$eventName]);
}
}
}
}
parent::removeListener($eventName, $listener);
}
/**
* {@inheritdoc}
*/
public function hasListeners($eventName = null)
{
if (null === $eventName) {
return $this->listenerIds || $this->listeners || parent::hasListeners();
}
if (isset($this->listenerIds[$eventName])) {
return true;
}
return parent::hasListeners($eventName);
}
/**
* {@inheritdoc}
*/
public function getListeners($eventName = null)
{
if (null === $eventName) {
foreach ($this->listenerIds as $serviceEventName => $args) {
$this->lazyLoad($serviceEventName);
}
} else {
$this->lazyLoad($eventName);
}
return parent::getListeners($eventName);
}
/**
* {@inheritdoc}
*/
public function getListenerPriority($eventName, $listener)
{
$this->lazyLoad($eventName);
return parent::getListenerPriority($eventName, $listener);
}
/**
* Adds a service as event subscriber.
*
* @param string $serviceId The service ID of the subscriber service
* @param string $class The service's class name (which must implement EventSubscriberInterface)
*/
public function addSubscriberService($serviceId, $class)
{
@trigger_error(sprintf('The %s class is deprecated since Symfony 3.3 and will be removed in 4.0. Use EventDispatcher with closure factories instead.', __CLASS__), E_USER_DEPRECATED);
foreach ($class::getSubscribedEvents() as $eventName => $params) {
if (is_string($params)) {
$this->listenerIds[$eventName][] = array($serviceId, $params, 0);
} elseif (is_string($params[0])) {
$this->listenerIds[$eventName][] = array($serviceId, $params[0], isset($params[1]) ? $params[1] : 0);
} else {
foreach ($params as $listener) {
$this->listenerIds[$eventName][] = array($serviceId, $listener[0], isset($listener[1]) ? $listener[1] : 0);
}
}
}
}
public function getContainer()
{
@trigger_error('The '.__METHOD__.'() method is deprecated since Symfony 3.3 as its class will be removed in 4.0. Inject the container or the services you need in your listeners/subscribers instead.', E_USER_DEPRECATED);
return $this->container;
}
/**
* Lazily loads listeners for this event from the dependency injection
* container.
*
* @param string $eventName The name of the event to dispatch. The name of
* the event is the name of the method that is
* invoked on listeners.
*/
protected function lazyLoad($eventName)
{
if (isset($this->listenerIds[$eventName])) {
foreach ($this->listenerIds[$eventName] as list($serviceId, $method, $priority)) {
$listener = $this->container->get($serviceId);
$key = $serviceId.'.'.$method;
if (!isset($this->listeners[$eventName][$key])) {
$this->addListener($eventName, array($listener, $method), $priority);
} elseif ($this->listeners[$eventName][$key] !== $listener) {
parent::removeListener($eventName, array($this->listeners[$eventName][$key], $method));
$this->addListener($eventName, array($listener, $method), $priority);
}
$this->listeners[$eventName][$key] = $listener;
}
}
}
}

View File

@@ -32,6 +32,7 @@ class TraceableEventDispatcher implements TraceableEventDispatcherInterface
private $called;
private $dispatcher;
private $wrappedListeners;
private $orphanedEvents;
public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null)
{
@@ -40,6 +41,7 @@ class TraceableEventDispatcher implements TraceableEventDispatcherInterface
$this->logger = $logger;
$this->called = array();
$this->wrappedListeners = array();
$this->orphanedEvents = array();
}
/**
@@ -207,6 +209,11 @@ class TraceableEventDispatcher implements TraceableEventDispatcherInterface
return $notCalled;
}
public function getOrphanedEvents(): array
{
return $this->orphanedEvents;
}
public function reset()
{
$this->called = array();
@@ -247,6 +254,12 @@ class TraceableEventDispatcher implements TraceableEventDispatcherInterface
private function preProcess($eventName)
{
if (!$this->dispatcher->hasListeners($eventName)) {
$this->orphanedEvents[] = $eventName;
return;
}
foreach ($this->dispatcher->getListeners($eventName) as $listener) {
$priority = $this->getListenerPriority($eventName, $listener);
$wrappedListener = new WrappedListener($listener, null, $this->stopwatch, $this);

View File

@@ -14,9 +14,9 @@ namespace Symfony\Component\EventDispatcher\Debug;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @deprecated since Symfony 4.1
*
* @method reset() Resets the trace.
* @author Fabien Potencier <fabien@symfony.com>
*/
interface TraceableEventDispatcherInterface extends EventDispatcherInterface
{
@@ -33,4 +33,9 @@ interface TraceableEventDispatcherInterface extends EventDispatcherInterface
* @return array An array of not called listeners
*/
public function getNotCalledListeners();
/**
* Resets the trace.
*/
public function reset();
}

View File

@@ -31,12 +31,7 @@ class RegisterListenersPass implements CompilerPassInterface
private $hotPathEvents = array();
private $hotPathTagName;
/**
* @param string $dispatcherService Service name of the event dispatcher in processed container
* @param string $listenerTag Tag name used for listener
* @param string $subscriberTag Tag name used for subscribers
*/
public function __construct($dispatcherService = 'event_dispatcher', $listenerTag = 'kernel.event_listener', $subscriberTag = 'kernel.event_subscriber')
public function __construct(string $dispatcherService = 'event_dispatcher', string $listenerTag = 'kernel.event_listener', string $subscriberTag = 'kernel.event_subscriber')
{
$this->dispatcherService = $dispatcherService;
$this->listenerTag = $listenerTag;
@@ -73,6 +68,10 @@ class RegisterListenersPass implements CompilerPassInterface
'/[^a-z0-9]/i',
), function ($matches) { return strtoupper($matches[0]); }, $event['event']);
$event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);
if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method']) && $r->hasMethod('__invoke')) {
$event['method'] = '__invoke';
}
}
$definition->addMethodCall('addListener', array($event['event'], array(new ServiceClosureArgument(new Reference($id)), $event['method']), $priority));
@@ -89,17 +88,15 @@ class RegisterListenersPass implements CompilerPassInterface
$def = $container->getDefinition($id);
// We must assume that the class value has been correctly filled, even if the service is created by a factory
$class = $container->getParameterBag()->resolveValue($def->getClass());
$interface = 'Symfony\Component\EventDispatcher\EventSubscriberInterface';
$class = $def->getClass();
if (!is_subclass_of($class, $interface)) {
if (!class_exists($class, false)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
if (!$r = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
$container->addObjectResource($class);
if (!$r->isSubclassOf(EventSubscriberInterface::class)) {
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, EventSubscriberInterface::class));
}
$class = $r->name;
ExtractingEventDispatcher::$subscriber = $class;
$extractingDispatcher->addSubscriber($extractingDispatcher);

View File

@@ -158,7 +158,7 @@ abstract class AbstractEventDispatcherTest extends TestCase
// be executed
// Manually set priority to enforce $this->listener to be called first
$this->dispatcher->addListener('post.foo', array($this->listener, 'postFoo'), 10);
$this->dispatcher->addListener('post.foo', array($otherListener, 'preFoo'));
$this->dispatcher->addListener('post.foo', array($otherListener, 'postFoo'));
$this->dispatcher->dispatch(self::postFoo);
$this->assertTrue($this->listener->postFooInvoked);
$this->assertFalse($otherListener->postFooInvoked);

View File

@@ -1,210 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher\Tests;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* @group legacy
*/
class ContainerAwareEventDispatcherTest extends AbstractEventDispatcherTest
{
protected function createEventDispatcher()
{
$container = new Container();
return new ContainerAwareEventDispatcher($container);
}
public function testAddAListenerService()
{
$event = new Event();
$service = $this->getMockBuilder('Symfony\Component\EventDispatcher\Tests\Service')->getMock();
$service
->expects($this->once())
->method('onEvent')
->with($event)
;
$container = new Container();
$container->set('service.listener', $service);
$dispatcher = new ContainerAwareEventDispatcher($container);
$dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
$dispatcher->dispatch('onEvent', $event);
}
public function testAddASubscriberService()
{
$event = new Event();
$service = $this->getMockBuilder('Symfony\Component\EventDispatcher\Tests\SubscriberService')->getMock();
$service
->expects($this->once())
->method('onEvent')
->with($event)
;
$service
->expects($this->once())
->method('onEventWithPriority')
->with($event)
;
$service
->expects($this->once())
->method('onEventNested')
->with($event)
;
$container = new Container();
$container->set('service.subscriber', $service);
$dispatcher = new ContainerAwareEventDispatcher($container);
$dispatcher->addSubscriberService('service.subscriber', 'Symfony\Component\EventDispatcher\Tests\SubscriberService');
$dispatcher->dispatch('onEvent', $event);
$dispatcher->dispatch('onEventWithPriority', $event);
$dispatcher->dispatch('onEventNested', $event);
}
public function testPreventDuplicateListenerService()
{
$event = new Event();
$service = $this->getMockBuilder('Symfony\Component\EventDispatcher\Tests\Service')->getMock();
$service
->expects($this->once())
->method('onEvent')
->with($event)
;
$container = new Container();
$container->set('service.listener', $service);
$dispatcher = new ContainerAwareEventDispatcher($container);
$dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'), 5);
$dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'), 10);
$dispatcher->dispatch('onEvent', $event);
}
public function testHasListenersOnLazyLoad()
{
$event = new Event();
$service = $this->getMockBuilder('Symfony\Component\EventDispatcher\Tests\Service')->getMock();
$container = new Container();
$container->set('service.listener', $service);
$dispatcher = new ContainerAwareEventDispatcher($container);
$dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
$service
->expects($this->once())
->method('onEvent')
->with($event)
;
$this->assertTrue($dispatcher->hasListeners());
if ($dispatcher->hasListeners('onEvent')) {
$dispatcher->dispatch('onEvent');
}
}
public function testGetListenersOnLazyLoad()
{
$service = $this->getMockBuilder('Symfony\Component\EventDispatcher\Tests\Service')->getMock();
$container = new Container();
$container->set('service.listener', $service);
$dispatcher = new ContainerAwareEventDispatcher($container);
$dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
$listeners = $dispatcher->getListeners();
$this->assertArrayHasKey('onEvent', $listeners);
$this->assertCount(1, $dispatcher->getListeners('onEvent'));
}
public function testRemoveAfterDispatch()
{
$service = $this->getMockBuilder('Symfony\Component\EventDispatcher\Tests\Service')->getMock();
$container = new Container();
$container->set('service.listener', $service);
$dispatcher = new ContainerAwareEventDispatcher($container);
$dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
$dispatcher->dispatch('onEvent', new Event());
$dispatcher->removeListener('onEvent', array($container->get('service.listener'), 'onEvent'));
$this->assertFalse($dispatcher->hasListeners('onEvent'));
}
public function testRemoveBeforeDispatch()
{
$service = $this->getMockBuilder('Symfony\Component\EventDispatcher\Tests\Service')->getMock();
$container = new Container();
$container->set('service.listener', $service);
$dispatcher = new ContainerAwareEventDispatcher($container);
$dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
$dispatcher->removeListener('onEvent', array($container->get('service.listener'), 'onEvent'));
$this->assertFalse($dispatcher->hasListeners('onEvent'));
}
}
class Service
{
public function onEvent(Event $e)
{
}
}
class SubscriberService implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
'onEvent' => 'onEvent',
'onEventWithPriority' => array('onEventWithPriority', 10),
'onEventNested' => array(array('onEventNested')),
);
}
public function onEvent(Event $e)
{
}
public function onEventWithPriority(Event $e)
{
}
public function onEventNested(Event $e)
{
}
}

View File

@@ -153,6 +153,31 @@ class TraceableEventDispatcherTest extends TestCase
$this->assertCount(2, $dispatcher->getCalledListeners());
}
public function testItReturnsNoOrphanedEventsWhenCreated()
{
$tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch());
$events = $tdispatcher->getOrphanedEvents();
$this->assertEmpty($events);
}
public function testItReturnsOrphanedEventsAfterDispatch()
{
$tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch());
$tdispatcher->dispatch('foo');
$events = $tdispatcher->getOrphanedEvents();
$this->assertCount(1, $events);
$this->assertEquals(array('foo'), $events);
}
public function testItDoesNotReturnHandledEvents()
{
$tdispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch());
$tdispatcher->addListener('foo', function () {});
$tdispatcher->dispatch('foo');
$events = $tdispatcher->getOrphanedEvents();
$this->assertEmpty($events);
}
public function testLogger()
{
$logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();

View File

@@ -27,29 +27,10 @@ class RegisterListenersPassTest extends TestCase
*/
public function testEventSubscriberWithoutInterface()
{
// one service, not implementing any interface
$services = array(
'my_event_subscriber' => array(0 => array()),
);
$definition = $this->getMockBuilder('Symfony\Component\DependencyInjection\Definition')->getMock();
$definition->expects($this->atLeastOnce())
->method('getClass')
->will($this->returnValue('stdClass'));
$builder = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder')->setMethods(array('hasDefinition', 'findTaggedServiceIds', 'getDefinition'))->getMock();
$builder->expects($this->any())
->method('hasDefinition')
->will($this->returnValue(true));
// We don't test kernel.event_listener here
$builder->expects($this->atLeastOnce())
->method('findTaggedServiceIds')
->will($this->onConsecutiveCalls(array(), $services));
$builder->expects($this->atLeastOnce())
->method('getDefinition')
->will($this->returnValue($definition));
$builder = new ContainerBuilder();
$builder->register('event_dispatcher');
$builder->register('my_event_subscriber', 'stdClass')
->addTag('kernel.event_subscriber');
$registerListenersPass = new RegisterListenersPass();
$registerListenersPass->process($builder);
@@ -61,31 +42,25 @@ class RegisterListenersPassTest extends TestCase
'my_event_subscriber' => array(0 => array()),
);
$definition = $this->getMockBuilder('Symfony\Component\DependencyInjection\Definition')->getMock();
$definition->expects($this->atLeastOnce())
->method('getClass')
->will($this->returnValue('Symfony\Component\EventDispatcher\Tests\DependencyInjection\SubscriberService'));
$builder = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder')->setMethods(array('hasDefinition', 'findTaggedServiceIds', 'getDefinition', 'findDefinition'))->getMock();
$builder->expects($this->any())
->method('hasDefinition')
->will($this->returnValue(true));
// We don't test kernel.event_listener here
$builder->expects($this->atLeastOnce())
->method('findTaggedServiceIds')
->will($this->onConsecutiveCalls(array(), $services));
$builder->expects($this->atLeastOnce())
->method('getDefinition')
->will($this->returnValue($definition));
$builder->expects($this->atLeastOnce())
->method('findDefinition')
->will($this->returnValue($definition));
$builder = new ContainerBuilder();
$eventDispatcherDefinition = $builder->register('event_dispatcher');
$builder->register('my_event_subscriber', 'Symfony\Component\EventDispatcher\Tests\DependencyInjection\SubscriberService')
->addTag('kernel.event_subscriber');
$registerListenersPass = new RegisterListenersPass();
$registerListenersPass->process($builder);
$expectedCalls = array(
array(
'addListener',
array(
'event',
array(new ServiceClosureArgument(new Reference('my_event_subscriber')), 'onEvent'),
0,
),
),
);
$this->assertEquals($expectedCalls, $eventDispatcherDefinition->getMethodCalls());
}
/**
@@ -166,6 +141,47 @@ class RegisterListenersPassTest extends TestCase
$registerListenersPass = new RegisterListenersPass();
$registerListenersPass->process($container);
}
public function testInvokableEventListener()
{
$container = new ContainerBuilder();
$container->register('foo', \stdClass::class)->addTag('kernel.event_listener', array('event' => 'foo.bar'));
$container->register('bar', InvokableListenerService::class)->addTag('kernel.event_listener', array('event' => 'foo.bar'));
$container->register('baz', InvokableListenerService::class)->addTag('kernel.event_listener', array('event' => 'event'));
$container->register('event_dispatcher', \stdClass::class);
$registerListenersPass = new RegisterListenersPass();
$registerListenersPass->process($container);
$definition = $container->getDefinition('event_dispatcher');
$expectedCalls = array(
array(
'addListener',
array(
'foo.bar',
array(new ServiceClosureArgument(new Reference('foo')), 'onFooBar'),
0,
),
),
array(
'addListener',
array(
'foo.bar',
array(new ServiceClosureArgument(new Reference('bar')), '__invoke'),
0,
),
),
array(
'addListener',
array(
'event',
array(new ServiceClosureArgument(new Reference('baz')), 'onEvent'),
0,
),
),
);
$this->assertEquals($expectedCalls, $definition->getMethodCalls());
}
}
class SubscriberService implements \Symfony\Component\EventDispatcher\EventSubscriberInterface
@@ -177,3 +193,14 @@ class SubscriberService implements \Symfony\Component\EventDispatcher\EventSubsc
);
}
}
class InvokableListenerService
{
public function __invoke()
{
}
public function onEvent()
{
}
}

View File

@@ -16,17 +16,17 @@
}
],
"require": {
"php": "^5.5.9|>=7.0.8"
"php": "^7.1.3"
},
"require-dev": {
"symfony/dependency-injection": "~3.3|~4.0",
"symfony/expression-language": "~2.8|~3.0|~4.0",
"symfony/config": "~2.8|~3.0|~4.0",
"symfony/stopwatch": "~2.8|~3.0|~4.0",
"symfony/dependency-injection": "~3.4|~4.0",
"symfony/expression-language": "~3.4|~4.0",
"symfony/config": "~3.4|~4.0",
"symfony/stopwatch": "~3.4|~4.0",
"psr/log": "~1.0"
},
"conflict": {
"symfony/dependency-injection": "<3.3"
"symfony/dependency-injection": "<3.4"
},
"suggest": {
"symfony/dependency-injection": "",
@@ -41,7 +41,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
"dev-master": "4.1-dev"
}
}
}

View File

@@ -297,6 +297,10 @@ class Finder implements \IteratorAggregate, \Countable
/**
* Excludes directories.
*
* Directories passed as argument must be relative to the ones defined with the `in()` method. For example:
*
* $finder->in(__DIR__)->exclude('ruby');
*
* @param string|array $dirs A directory path or an array of directories
*
* @return $this
@@ -313,6 +317,8 @@ class Finder implements \IteratorAggregate, \Countable
/**
* Excludes "hidden" directories and files (starting with a dot).
*
* This option is enabled by default.
*
* @param bool $ignoreDotFiles Whether to exclude "hidden" files or not
*
* @return $this
@@ -333,6 +339,8 @@ class Finder implements \IteratorAggregate, \Countable
/**
* Forces the finder to ignore version control directories.
*
* This option is enabled by default.
*
* @param bool $ignoreVCS Whether to exclude VCS files or not
*
* @return $this
@@ -532,9 +540,9 @@ class Finder implements \IteratorAggregate, \Countable
foreach ((array) $dirs as $dir) {
if (is_dir($dir)) {
$resolvedDirs[] = $dir;
$resolvedDirs[] = $this->normalizeDir($dir);
} elseif ($glob = glob($dir, (defined('GLOB_BRACE') ? GLOB_BRACE : 0) | GLOB_ONLYDIR)) {
$resolvedDirs = array_merge($resolvedDirs, $glob);
$resolvedDirs = array_merge($resolvedDirs, array_map(array($this, 'normalizeDir'), $glob));
} else {
throw new \InvalidArgumentException(sprintf('The "%s" directory does not exist.', $dir));
}
@@ -720,4 +728,16 @@ class Finder implements \IteratorAggregate, \Countable
return $iterator;
}
/**
* Normalizes given directory names by removing trailing slashes.
*
* @param string $dir
*
* @return string
*/
private function normalizeDir($dir)
{
return rtrim($dir, '/'.\DIRECTORY_SEPARATOR);
}
}

View File

@@ -66,12 +66,11 @@ class SplFileInfo extends \SplFileInfo
*/
public function getContents()
{
$level = error_reporting(0);
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
$content = file_get_contents($this->getPathname());
error_reporting($level);
restore_error_handler();
if (false === $content) {
$error = error_get_last();
throw new \RuntimeException($error['message']);
throw new \RuntimeException($error);
}
return $content;

View File

@@ -46,6 +46,45 @@ class FinderTest extends Iterator\RealIteratorTestCase
$this->assertIterator($this->toAbsolute(array('foo/bar.tmp', 'test.php', 'test.py', 'foo bar')), $finder->in(self::$tmpDir)->getIterator());
}
public function testRemoveTrailingSlash()
{
$finder = $this->buildFinder();
$expected = $this->toAbsolute(array('foo/bar.tmp', 'test.php', 'test.py', 'foo bar'));
$in = self::$tmpDir.'//';
$this->assertIterator($expected, $finder->in($in)->files()->getIterator());
}
public function testSymlinksNotResolved()
{
if ('\\' === DIRECTORY_SEPARATOR) {
$this->markTestSkipped('symlinks are not supported on Windows');
}
$finder = $this->buildFinder();
symlink($this->toAbsolute('foo'), $this->toAbsolute('baz'));
$expected = $this->toAbsolute(array('baz/bar.tmp'));
$in = self::$tmpDir.'/baz/';
try {
$this->assertIterator($expected, $finder->in($in)->files()->getIterator());
unlink($this->toAbsolute('baz'));
} catch (\Exception $e) {
unlink($this->toAbsolute('baz'));
throw $e;
}
}
public function testBackPathNotNormalized()
{
$finder = $this->buildFinder();
$expected = $this->toAbsolute(array('foo/../foo/bar.tmp'));
$in = self::$tmpDir.'/foo/../foo/';
$this->assertIterator($expected, $finder->in($in)->files()->getIterator());
}
public function testDepth()
{
$finder = $this->buildFinder();
@@ -261,7 +300,7 @@ class FinderTest extends Iterator\RealIteratorTestCase
public function testInWithGlob()
{
$finder = $this->buildFinder();
$finder->in(array(__DIR__.'/Fixtures/*/B/C', __DIR__.'/Fixtures/*/*/B/C'))->getIterator();
$finder->in(array(__DIR__.'/Fixtures/*/B/C/', __DIR__.'/Fixtures/*/*/B/C/'))->getIterator();
$this->assertIterator($this->toAbsoluteFixtures(array('A/B/C/abc.dat', 'copy/A/B/C/abc.dat.copy')), $finder);
}

View File

@@ -145,12 +145,12 @@ class Cookie
$str = ($this->isRaw() ? $this->getName() : urlencode($this->getName())).'=';
if ('' === (string) $this->getValue()) {
$str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; max-age=-31536001';
$str .= 'deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0';
} else {
$str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue());
if (0 !== $this->getExpiresTime()) {
$str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; max-age='.$this->getMaxAge();
$str .= '; expires='.gmdate('D, d-M-Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge();
}
}
@@ -224,7 +224,9 @@ class Cookie
*/
public function getMaxAge()
{
return 0 !== $this->expire ? $this->expire - time() : 0;
$maxAge = $this->expire - time();
return 0 >= $maxAge ? 0 : $maxAge;
}
/**

View File

@@ -93,9 +93,11 @@ class File extends \SplFileInfo
{
$target = $this->getTargetFile($directory, $name);
if (!@rename($this->getPathname(), $target)) {
$error = error_get_last();
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error['message'])));
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
$renamed = rename($this->getPathname(), $target);
restore_error_handler();
if (!$renamed) {
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error)));
}
@chmod($target, 0666 & ~umask());

View File

@@ -43,7 +43,21 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
*/
public static function isSupported()
{
return '\\' !== DIRECTORY_SEPARATOR && function_exists('passthru') && function_exists('escapeshellarg');
static $supported = null;
if (null !== $supported) {
return $supported;
}
if ('\\' === DIRECTORY_SEPARATOR || !function_exists('passthru') || !function_exists('escapeshellarg')) {
return $supported = false;
}
ob_start();
passthru('command -v file', $exitStatus);
$binPath = trim(ob_get_clean());
return $supported = 0 === $exitStatus && '' !== $binPath;
}
/**

View File

@@ -599,6 +599,7 @@ class MimeTypeExtensionGuesser implements ExtensionGuesserInterface
'application/x-xliff+xml' => 'xlf',
'application/x-xpinstall' => 'xpi',
'application/x-xz' => 'xz',
'application/x-zip-compressed' => 'zip',
'application/x-zmachine' => 'z1',
'application/xaml+xml' => 'xaml',
'application/xcap-diff+xml' => 'xdf',

View File

@@ -80,13 +80,8 @@ class MimeTypeGuesser implements MimeTypeGuesserInterface
*/
private function __construct()
{
if (FileBinaryMimeTypeGuesser::isSupported()) {
$this->register(new FileBinaryMimeTypeGuesser());
}
if (FileinfoMimeTypeGuesser::isSupported()) {
$this->register(new FileinfoMimeTypeGuesser());
}
$this->register(new FileBinaryMimeTypeGuesser());
$this->register(new FileinfoMimeTypeGuesser());
}
/**
@@ -125,18 +120,14 @@ class MimeTypeGuesser implements MimeTypeGuesserInterface
throw new AccessDeniedException($path);
}
if (!$this->guessers) {
$msg = 'Unable to guess the mime type as no guessers are available';
if (!FileinfoMimeTypeGuesser::isSupported()) {
$msg .= ' (Did you enable the php_fileinfo extension?)';
}
throw new \LogicException($msg);
}
foreach ($this->guessers as $guesser) {
if (null !== $mimeType = $guesser->guess($path)) {
return $mimeType;
}
}
if (2 === \count($this->guessers) && !FileBinaryMimeTypeGuesser::isSupported() && !FileinfoMimeTypeGuesser::isSupported()) {
throw new \LogicException('Unable to guess the mime type as no guessers are available (Did you enable the php_fileinfo extension?)');
}
}
}

View File

@@ -192,9 +192,11 @@ class UploadedFile extends File
$target = $this->getTargetFile($directory, $name);
if (!@move_uploaded_file($this->getPathname(), $target)) {
$error = error_get_last();
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error['message'])));
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
$moved = move_uploaded_file($this->getPathname(), $target);
restore_error_handler();
if (!$moved) {
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error)));
}
@chmod($target, 0666 & ~umask());

View File

@@ -144,7 +144,7 @@ class Request
public $headers;
/**
* @var string|resource
* @var string|resource|false|null
*/
protected $content;
@@ -242,13 +242,13 @@ class Request
);
/**
* @param array $query The GET parameters
* @param array $request The POST parameters
* @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
* @param array $cookies The COOKIE parameters
* @param array $files The FILES parameters
* @param array $server The SERVER parameters
* @param string|resource $content The raw body data
* @param array $query The GET parameters
* @param array $request The POST parameters
* @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
* @param array $cookies The COOKIE parameters
* @param array $files The FILES parameters
* @param array $server The SERVER parameters
* @param string|resource|null $content The raw body data
*/
public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null)
{
@@ -260,13 +260,13 @@ class Request
*
* This method also re-initializes all properties.
*
* @param array $query The GET parameters
* @param array $request The POST parameters
* @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
* @param array $cookies The COOKIE parameters
* @param array $files The FILES parameters
* @param array $server The SERVER parameters
* @param string|resource $content The raw body data
* @param array $query The GET parameters
* @param array $request The POST parameters
* @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
* @param array $cookies The COOKIE parameters
* @param array $files The FILES parameters
* @param array $server The SERVER parameters
* @param string|resource|null $content The raw body data
*/
public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null)
{
@@ -329,13 +329,13 @@ class Request
* The information contained in the URI always take precedence
* over the other information (server and parameters).
*
* @param string $uri The URI
* @param string $method The HTTP method
* @param array $parameters The query (GET) or request (POST) parameters
* @param array $cookies The request cookies ($_COOKIE)
* @param array $files The request files ($_FILES)
* @param array $server The server parameters ($_SERVER)
* @param string|resource $content The raw body data
* @param string $uri The URI
* @param string $method The HTTP method
* @param array $parameters The query (GET) or request (POST) parameters
* @param array $cookies The request cookies ($_COOKIE)
* @param array $files The request files ($_FILES)
* @param array $server The server parameters ($_SERVER)
* @param string|resource|null $content The raw body data
*
* @return static
*/
@@ -557,7 +557,7 @@ class Request
*/
public function overrideGlobals()
{
$this->server->set('QUERY_STRING', static::normalizeQueryString(http_build_query($this->query->all(), null, '&')));
$this->server->set('QUERY_STRING', static::normalizeQueryString(http_build_query($this->query->all(), '', '&')));
$_GET = $this->query->all();
$_POST = $this->request->all();
@@ -641,7 +641,7 @@ class Request
public static function setTrustedHosts(array $hostPatterns)
{
self::$trustedHostPatterns = array_map(function ($hostPattern) {
return sprintf('#%s#i', $hostPattern);
return sprintf('{%s}i', $hostPattern);
}, $hostPatterns);
// we need to reset trusted hosts on trusted host patterns change
self::$trustedHosts = array();
@@ -1378,7 +1378,7 @@ class Request
*
* @param string $format The format
*
* @return string The associated mime type (null if not found)
* @return string|null The associated mime type (null if not found)
*/
public function getMimeType($format)
{

View File

@@ -21,6 +21,7 @@ class Response
const HTTP_CONTINUE = 100;
const HTTP_SWITCHING_PROTOCOLS = 101;
const HTTP_PROCESSING = 102; // RFC2518
const HTTP_EARLY_HINTS = 103; // RFC8297
const HTTP_OK = 200;
const HTTP_CREATED = 201;
const HTTP_ACCEPTED = 202;
@@ -327,7 +328,7 @@ class Response
}
// headers
foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) {
foreach ($this->headers->allPreserveCase() as $name => $values) {
foreach ($values as $value) {
header($name.': '.$value, false, $this->statusCode);
}
@@ -336,15 +337,6 @@ class Response
// status
header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
// cookies
foreach ($this->headers->getCookies() as $cookie) {
if ($cookie->isRaw()) {
setrawcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
} else {
setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly());
}
}
return $this;
}
@@ -372,7 +364,7 @@ class Response
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif ('cli' !== PHP_SAPI) {
} elseif (!\in_array(PHP_SAPI, array('cli', 'phpdbg'), true)) {
static::closeOutputBuffers(0, true);
}
@@ -519,13 +511,19 @@ class Response
}
/**
* Returns true if the response is worth caching under any circumstance.
* Returns true if the response may safely be kept in a shared (surrogate) cache.
*
* Responses marked "private" with an explicit Cache-Control directive are
* considered uncacheable.
*
* Responses with neither a freshness lifetime (Expires, max-age) nor cache
* validator (Last-Modified, ETag) are considered uncacheable.
* validator (Last-Modified, ETag) are considered uncacheable because there is
* no way to tell when or how to remove them from the cache.
*
* Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation,
* for example "status codes that are defined as cacheable by default [...]
* can be reused by a cache with heuristic expiration unless otherwise indicated"
* (https://tools.ietf.org/html/rfc7231#section-6.1)
*
* @return bool true if the response is worth caching, false otherwise
*

View File

@@ -80,7 +80,9 @@ class MemcachedSessionHandler extends AbstractSessionHandler
*/
public function updateTimestamp($sessionId, $data)
{
return $this->memcached->touch($this->prefix.$sessionId, time() + $this->ttl);
$this->memcached->touch($this->prefix.$sessionId, time() + $this->ttl);
return true;
}
/**

View File

@@ -164,7 +164,7 @@ class PdoSessionHandler extends AbstractSessionHandler
* * db_connection_options: An array of driver-specific connection options [default: array()]
* * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
*
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or null
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null
* @param array $options An associative array of options
*
* @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
@@ -178,6 +178,8 @@ class PdoSessionHandler extends AbstractSessionHandler
$this->pdo = $pdoOrDsn;
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
} elseif (is_string($pdoOrDsn) && false !== strpos($pdoOrDsn, '://')) {
$this->dsn = $this->buildDsnFromUrl($pdoOrDsn);
} else {
$this->dsn = $pdoOrDsn;
}
@@ -431,6 +433,102 @@ class PdoSessionHandler extends AbstractSessionHandler
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
}
/**
* Builds a PDO DSN from a URL-like connection string.
*
* @param string $dsnOrUrl
*
* @return string
*
* @todo implement missing support for oci DSN (which look totally different from other PDO ones)
*/
private function buildDsnFromUrl($dsnOrUrl)
{
// (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid
$url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl);
$params = parse_url($url);
if (false === $params) {
return $dsnOrUrl; // If the URL is not valid, let's assume it might be a DSN already.
}
$params = array_map('rawurldecode', $params);
// Override the default username and password. Values passed through options will still win over these in the constructor.
if (isset($params['user'])) {
$this->username = $params['user'];
}
if (isset($params['pass'])) {
$this->password = $params['pass'];
}
if (!isset($params['scheme'])) {
throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler');
}
$driverAliasMap = array(
'mssql' => 'sqlsrv',
'mysql2' => 'mysql', // Amazon RDS, for some weird reason
'postgres' => 'pgsql',
'postgresql' => 'pgsql',
'sqlite3' => 'sqlite',
);
$driver = isset($driverAliasMap[$params['scheme']]) ? $driverAliasMap[$params['scheme']] : $params['scheme'];
// Doctrine DBAL supports passing its internal pdo_* driver names directly too (allowing both dashes and underscores). This allows supporting the same here.
if (0 === strpos($driver, 'pdo_') || 0 === strpos($driver, 'pdo-')) {
$driver = substr($driver, 4);
}
switch ($driver) {
case 'mysql':
case 'pgsql':
$dsn = $driver.':';
if (isset($params['host']) && '' !== $params['host']) {
$dsn .= 'host='.$params['host'].';';
}
if (isset($params['port']) && '' !== $params['port']) {
$dsn .= 'port='.$params['port'].';';
}
if (isset($params['path'])) {
$dbName = substr($params['path'], 1); // Remove the leading slash
$dsn .= 'dbname='.$dbName.';';
}
return $dsn;
case 'sqlite':
return 'sqlite:'.substr($params['path'], 1);
case 'sqlsrv':
$dsn = 'sqlsrv:server=';
if (isset($params['host'])) {
$dsn .= $params['host'];
}
if (isset($params['port']) && '' !== $params['port']) {
$dsn .= ','.$params['port'];
}
if (isset($params['path'])) {
$dbName = substr($params['path'], 1); // Remove the leading slash
$dsn .= ';Database='.$dbName;
}
return $dsn;
default:
throw new \InvalidArgumentException(sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme']));
}
}
/**
* Helper method to begin a transaction.
*
@@ -518,6 +616,7 @@ class PdoSessionHandler extends AbstractSessionHandler
$selectSql = $this->getSelectSql();
$selectStmt = $this->pdo->prepare($selectSql);
$selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$insertStmt = null;
do {
$selectStmt->execute();
@@ -533,6 +632,11 @@ class PdoSessionHandler extends AbstractSessionHandler
return is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0];
}
if (null !== $insertStmt) {
$this->rollback();
throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.');
}
if (!ini_get('session.use_strict_mode') && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) {
// In strict mode, session fixation is not possible: new sessions always start with a unique
// random id, so that concurrency is not possible and this code path can be skipped.
@@ -578,14 +682,16 @@ class PdoSessionHandler extends AbstractSessionHandler
{
switch ($this->driver) {
case 'mysql':
// MySQL 5.7.5 and later enforces a maximum length on lock names of 64 characters. Previously, no limit was enforced.
$lockId = \substr($sessionId, 0, 64);
// should we handle the return value? 0 on timeout, null on error
// we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout
$stmt = $this->pdo->prepare('SELECT GET_LOCK(:key, 50)');
$stmt->bindValue(':key', $sessionId, \PDO::PARAM_STR);
$stmt->bindValue(':key', $lockId, \PDO::PARAM_STR);
$stmt->execute();
$releaseStmt = $this->pdo->prepare('DO RELEASE_LOCK(:key)');
$releaseStmt->bindValue(':key', $sessionId, \PDO::PARAM_STR);
$releaseStmt->bindValue(':key', $lockId, \PDO::PARAM_STR);
return $releaseStmt;
case 'pgsql':

View File

@@ -349,7 +349,7 @@ class NativeSessionStorage implements SessionStorageInterface
}
$validOptions = array_flip(array(
'cache_limiter', 'cache_expire', 'cookie_domain', 'cookie_httponly',
'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly',
'cookie_lifetime', 'cookie_path', 'cookie_secure',
'entropy_file', 'entropy_length', 'gc_divisor',
'gc_maxlifetime', 'gc_probability', 'hash_bits_per_character',
@@ -357,13 +357,13 @@ class NativeSessionStorage implements SessionStorageInterface
'serialize_handler', 'use_strict_mode', 'use_cookies',
'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled',
'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name',
'upload_progress.freq', 'upload_progress.min-freq', 'url_rewriter.tags',
'upload_progress.freq', 'upload_progress.min_freq', 'url_rewriter.tags',
'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags',
));
foreach ($options as $key => $value) {
if (isset($validOptions[$key])) {
ini_set('session.'.$key, $value);
ini_set('url_rewriter.tags' !== $key ? 'session.'.$key : $key, $value);
}
}
}

View File

@@ -162,13 +162,13 @@ class CookieTest extends TestCase
public function testToString()
{
$cookie = new Cookie('foo', 'bar', $expire = strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true);
$this->assertEquals('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; max-age='.($expire - time()).'; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie');
$this->assertEquals('foo=bar; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() returns string representation of the cookie');
$cookie = new Cookie('foo', 'bar with white spaces', strtotime('Fri, 20-May-2011 15:25:52 GMT'), '/', '.myfoodomain.com', true);
$this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; max-age='.($expire - time()).'; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');
$this->assertEquals('foo=bar%20with%20white%20spaces; expires=Fri, 20-May-2011 15:25:52 GMT; Max-Age=0; path=/; domain=.myfoodomain.com; secure; httponly', (string) $cookie, '->__toString() encodes the value of the cookie according to RFC 3986 (white space = %20)');
$cookie = new Cookie('foo', null, 1, '/admin/', '.myfoodomain.com');
$this->assertEquals('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; max-age='.($expire - time()).'; path=/admin/; domain=.myfoodomain.com; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
$this->assertEquals('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', $expire = time() - 31536001).'; Max-Age=0; path=/admin/; domain=.myfoodomain.com; httponly', (string) $cookie, '->__toString() returns string representation of a cleared cookie if value is NULL');
$cookie = new Cookie('foo', 'bar', 0, '/', '');
$this->assertEquals('foo=bar; path=/; httponly', (string) $cookie);
@@ -194,7 +194,7 @@ class CookieTest extends TestCase
$this->assertEquals($expire - time(), $cookie->getMaxAge());
$cookie = new Cookie('foo', 'bar', $expire = time() - 100);
$this->assertEquals($expire - time(), $cookie->getMaxAge());
$this->assertEquals(0, $cookie->getMaxAge());
}
public function testFromString()

View File

@@ -0,0 +1,39 @@
<?php
use Symfony\Component\HttpFoundation\Response;
$parent = __DIR__;
while (!@file_exists($parent.'/vendor/autoload.php')) {
if (!@file_exists($parent)) {
// open_basedir restriction in effect
break;
}
if ($parent === dirname($parent)) {
echo "vendor/autoload.php not found\n";
exit(1);
}
$parent = dirname($parent);
}
require $parent.'/vendor/autoload.php';
error_reporting(-1);
ini_set('html_errors', 0);
ini_set('display_errors', 1);
header_remove('X-Powered-By');
header('Content-Type: text/plain; charset=utf-8');
register_shutdown_function(function () {
echo "\n";
session_write_close();
print_r(headers_list());
echo "shutdown\n";
});
ob_start();
$r = new Response();
$r->headers->set('Date', 'Sat, 12 Nov 1955 20:04:00 GMT');
return $r;

View File

@@ -0,0 +1,11 @@
Warning: Expiry date cannot have a year greater than 9999 in %scookie_max_age.php on line 10
Array
(
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: no-cache, private
[2] => Date: Sat, 12 Nov 1955 20:04:00 GMT
[3] => Set-Cookie: foo=bar; expires=Sat, 01-Jan-10000 02:46:40 GMT; Max-Age=%d; path=/
)
shutdown

View File

@@ -0,0 +1,10 @@
<?php
use Symfony\Component\HttpFoundation\Cookie;
$r = require __DIR__.'/common.inc';
$r->headers->setCookie(new Cookie('foo', 'bar', 253402310800, '', null, false, false));
$r->sendHeaders();
setcookie('foo2', 'bar', 253402310800, '/');

View File

@@ -0,0 +1,10 @@
Array
(
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: no-cache, private
[2] => Date: Sat, 12 Nov 1955 20:04:00 GMT
[3] => Set-Cookie: ?*():@&+$/%#[]=?*():@&+$/%#[]; path=/
[4] => Set-Cookie: ?*():@&+$/%#[]=?*():@&+$/%#[]; path=/
)
shutdown

View File

@@ -0,0 +1,12 @@
<?php
use Symfony\Component\HttpFoundation\Cookie;
$r = require __DIR__.'/common.inc';
$str = '?*():@&+$/%#[]';
$r->headers->setCookie(new Cookie($str, $str, 0, '/', null, false, false, true));
$r->sendHeaders();
setrawcookie($str, $str, 0, '/', null, false, false);

View File

@@ -0,0 +1,9 @@
Array
(
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: no-cache, private
[2] => Date: Sat, 12 Nov 1955 20:04:00 GMT
[3] => Set-Cookie: CookieSamesiteLaxTest=LaxValue; path=/; httponly; samesite=lax
)
shutdown

View File

@@ -0,0 +1,8 @@
<?php
use Symfony\Component\HttpFoundation\Cookie;
$r = require __DIR__.'/common.inc';
$r->headers->setCookie(new Cookie('CookieSamesiteLaxTest', 'LaxValue', 0, '/', null, false, true, false, Cookie::SAMESITE_LAX));
$r->sendHeaders();

View File

@@ -0,0 +1,9 @@
Array
(
[0] => Content-Type: text/plain; charset=utf-8
[1] => Cache-Control: no-cache, private
[2] => Date: Sat, 12 Nov 1955 20:04:00 GMT
[3] => Set-Cookie: CookieSamesiteStrictTest=StrictValue; path=/; httponly; samesite=strict
)
shutdown

View File

@@ -0,0 +1,8 @@
<?php
use Symfony\Component\HttpFoundation\Cookie;
$r = require __DIR__.'/common.inc';
$r->headers->setCookie(new Cookie('CookieSamesiteStrictTest', 'StrictValue', 0, '/', null, false, true, false, Cookie::SAMESITE_STRICT));
$r->sendHeaders();

Some files were not shown because too many files have changed in this diff Show More