This commit is contained in:
2020-02-01 16:47:12 +07:00
commit 4c619ad6e6
16739 changed files with 3329179 additions and 0 deletions

58
vendor/phpspec/php-diff/README vendored Normal file
View File

@@ -0,0 +1,58 @@
PHP Diff Class
--------------
Introduction
------------
A comprehensive library for generating differences between
two hashable objects (strings or arrays). Generated differences can be
rendered in all of the standard formats including:
* Unified
* Context
* Inline HTML
* Side by Side HTML
The logic behind the core of the diff engine (ie, the sequence matcher)
is primarily based on the Python difflib package. The reason for doing
so is primarily because of its high degree of accuracy.
Example Use
-----------
A quick usage example can be found in the example/ directory and under
example.php.
More complete documentation will be available shortly.
Todo
----
* Ability to ignore blank line changes
* 3 way diff support
* Performance optimizations
License (BSD License)
---------------------
Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of the Chris Boulton nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

25
vendor/phpspec/php-diff/composer.json vendored Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "phpspec/php-diff",
"type": "library",
"description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).",
"license": "BSD-3-Clause",
"authors": [
{
"name": "Chris Boulton",
"homepage": "http://github.com/chrisboulton"
}
],
"autoload": {
"psr-0": {
"Diff": "lib/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
}
}

13
vendor/phpspec/php-diff/example/a.txt vendored Normal file
View File

@@ -0,0 +1,13 @@
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>Hello World!</title>
</head>
<body>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<h2>A heading we'll be removing</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</body>
</html>

14
vendor/phpspec/php-diff/example/b.txt vendored Normal file
View File

@@ -0,0 +1,14 @@
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>Goodbye Cruel World!</title>
</head>
<body>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Just a small amount of new text...</p>
</body>
</html>

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>PHP LibDiff - Examples</title>
<link rel="stylesheet" href="styles.css" type="text/css" charset="utf-8"/>
</head>
<body>
<h1>PHP LibDiff - Examples</h1>
<hr />
<?php
// Include the diff class
require_once dirname(__FILE__).'/../lib/Diff.php';
// Include two sample files for comparison
$a = explode("\n", file_get_contents(dirname(__FILE__).'/a.txt'));
$b = explode("\n", file_get_contents(dirname(__FILE__).'/b.txt'));
// Options for generating the diff
$options = array(
//'ignoreWhitespace' => true,
//'ignoreCase' => true,
);
// Initialize the diff class
$diff = new Diff($a, $b, $options);
?>
<h2>Side by Side Diff</h2>
<?php
// Generate a side by side diff
require_once dirname(__FILE__).'/../lib/Diff/Renderer/Html/SideBySide.php';
$renderer = new Diff_Renderer_Html_SideBySide;
echo $diff->Render($renderer);
?>
<h2>Inline Diff</h2>
<?php
// Generate an inline diff
require_once dirname(__FILE__).'/../lib/Diff/Renderer/Html/Inline.php';
$renderer = new Diff_Renderer_Html_Inline;
echo $diff->render($renderer);
?>
<h2>Unified Diff</h2>
<pre><?php
// Generate a unified diff
require_once dirname(__FILE__).'/../lib/Diff/Renderer/Text/Unified.php';
$renderer = new Diff_Renderer_Text_Unified;
echo htmlspecialchars($diff->render($renderer));
?>
</pre>
<h2>Context Diff</h2>
<pre><?php
// Generate a context diff
require_once dirname(__FILE__).'/../lib/Diff/Renderer/Text/Context.php';
$renderer = new Diff_Renderer_Text_Context;
echo htmlspecialchars($diff->render($renderer));
?>
</pre>
</body>
</html>

View File

@@ -0,0 +1,93 @@
body {
background: #fff;
font-family: Arial;
font-size: 12px;
}
.Differences {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
empty-cells: show;
}
.Differences thead th {
text-align: left;
border-bottom: 1px solid #000;
background: #aaa;
color: #000;
padding: 4px;
}
.Differences tbody th {
text-align: right;
background: #ccc;
width: 4em;
padding: 1px 2px;
border-right: 1px solid #000;
vertical-align: top;
font-size: 13px;
}
.Differences td {
padding: 1px 2px;
font-family: Consolas, monospace;
font-size: 13px;
}
.DifferencesSideBySide .ChangeInsert td.Left {
background: #dfd;
}
.DifferencesSideBySide .ChangeInsert td.Right {
background: #cfc;
}
.DifferencesSideBySide .ChangeDelete td.Left {
background: #f88;
}
.DifferencesSideBySide .ChangeDelete td.Right {
background: #faa;
}
.DifferencesSideBySide .ChangeReplace .Left {
background: #fe9;
}
.DifferencesSideBySide .ChangeReplace .Right {
background: #fd8;
}
.Differences ins, .Differences del {
text-decoration: none;
}
.DifferencesSideBySide .ChangeReplace ins, .DifferencesSideBySide .ChangeReplace del {
background: #fc0;
}
.Differences .Skipped {
background: #f7f7f7;
}
.DifferencesInline .ChangeReplace .Left,
.DifferencesInline .ChangeDelete .Left {
background: #fdd;
}
.DifferencesInline .ChangeReplace .Right,
.DifferencesInline .ChangeInsert .Right {
background: #dfd;
}
.DifferencesInline .ChangeReplace ins {
background: #9e9;
}
.DifferencesInline .ChangeReplace del {
background: #e99;
}
pre {
width: 100%;
overflow: auto;
}

177
vendor/phpspec/php-diff/lib/Diff.php vendored Normal file
View File

@@ -0,0 +1,177 @@
<?php
/**
* Diff
*
* A comprehensive library for generating differences between two strings
* in multiple formats (unified, side by side HTML etc)
*
* PHP version 5
*
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the Chris Boulton nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package Diff
* @author Chris Boulton <chris.boulton@interspire.com>
* @copyright (c) 2009 Chris Boulton
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
* @version 1.1
* @link http://github.com/chrisboulton/php-diff
*/
class Diff
{
/**
* @var array The "old" sequence to use as the basis for the comparison.
*/
private $a = null;
/**
* @var array The "new" sequence to generate the changes for.
*/
private $b = null;
/**
* @var array Array containing the generated opcodes for the differences between the two items.
*/
private $groupedCodes = null;
/**
* @var array Associative array of the default options available for the diff class and their default value.
*/
private $defaultOptions = array(
'context' => 3,
'ignoreNewLines' => false,
'ignoreWhitespace' => false,
'ignoreCase' => false
);
/**
* @var array Array of the options that have been applied for generating the diff.
*/
private $options = array();
/**
* The constructor.
*
* @param array $a Array containing the lines of the first string to compare.
* @param array $b Array containing the lines for the second string to compare.
* @param array $options
*/
public function __construct($a, $b, $options=array())
{
$this->a = $a;
$this->b = $b;
$this->options = array_merge($this->defaultOptions, $options);
}
/**
* Render a diff using the supplied rendering class and return it.
*
* @param Diff_Renderer_Abstract $renderer An instance of the rendering object to use for generating the diff.
* @return mixed The generated diff. Exact return value depends on the rendered.
*/
public function render(Diff_Renderer_Abstract $renderer)
{
$renderer->diff = $this;
return $renderer->render();
}
/**
* Get a range of lines from $start to $end from the first comparison string
* and return them as an array. If no values are supplied, the entire string
* is returned. It's also possible to specify just one line to return only
* that line.
*
* @param int $start The starting number.
* @param int $end The ending number. If not supplied, only the item in $start will be returned.
* @return array Array of all of the lines between the specified range.
*/
public function getA($start=0, $end=null)
{
if($start == 0 && $end === null) {
return $this->a;
}
if($end === null) {
$length = 1;
}
else {
$length = $end - $start;
}
return array_slice($this->a, $start, $length);
}
/**
* Get a range of lines from $start to $end from the second comparison string
* and return them as an array. If no values are supplied, the entire string
* is returned. It's also possible to specify just one line to return only
* that line.
*
* @param int $start The starting number.
* @param int $end The ending number. If not supplied, only the item in $start will be returned.
* @return array Array of all of the lines between the specified range.
*/
public function getB($start=0, $end=null)
{
if($start == 0 && $end === null) {
return $this->b;
}
if($end === null) {
$length = 1;
}
else {
$length = $end - $start;
}
return array_slice($this->b, $start, $length);
}
/**
* Generate a list of the compiled and grouped opcodes for the differences between the
* two strings. Generally called by the renderer, this class instantiates the sequence
* matcher and performs the actual diff generation and return an array of the opcodes
* for it. Once generated, the results are cached in the diff class instance.
*
* @return array Array of the grouped opcodes for the generated diff.
*/
public function getGroupedOpcodes()
{
if(!is_null($this->groupedCodes)) {
return $this->groupedCodes;
}
require_once dirname(__FILE__).'/Diff/SequenceMatcher.php';
$sequenceMatcher = new Diff_SequenceMatcher($this->a, $this->b, null, $this->options);
$this->groupedCodes = $sequenceMatcher->getGroupedOpcodes();
return $this->groupedCodes;
}
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* Abstract class for diff renderers in PHP DiffLib.
*
* PHP version 5
*
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the Chris Boulton nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package DiffLib
* @author Chris Boulton <chris.boulton@interspire.com>
* @copyright (c) 2009 Chris Boulton
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
* @version 1.1
* @link http://github.com/chrisboulton/php-diff
*/
abstract class Diff_Renderer_Abstract
{
/**
* @var object Instance of the diff class that this renderer is generating the rendered diff for.
*/
public $diff;
/**
* @var array Array of the default options that apply to this renderer.
*/
protected $defaultOptions = array();
/**
* @var array Array containing the user applied and merged default options for the renderer.
*/
protected $options = array();
/**
* The constructor. Instantiates the rendering engine and if options are passed,
* sets the options for the renderer.
*
* @param array $options Optionally, an array of the options for the renderer.
*/
public function __construct(array $options = array())
{
$this->setOptions($options);
}
/**
* Set the options of the renderer to those supplied in the passed in array.
* Options are merged with the default to ensure that there aren't any missing
* options.
*
* @param array $options Array of options to set.
*/
public function setOptions(array $options)
{
$this->options = array_merge($this->defaultOptions, $options);
}
}

View File

@@ -0,0 +1,233 @@
<?php
/**
* Base renderer for rendering HTML based diffs for PHP DiffLib.
*
* PHP version 5
*
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the Chris Boulton nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package DiffLib
* @author Chris Boulton <chris.boulton@interspire.com>
* @copyright (c) 2009 Chris Boulton
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
* @version 1.1
* @link http://github.com/chrisboulton/php-diff
*/
require_once dirname(__FILE__).'/../Abstract.php';
class Diff_Renderer_Html_Array extends Diff_Renderer_Abstract
{
/**
* @var array Array of the default options that apply to this renderer.
*/
protected $defaultOptions = array(
'tabSize' => 4
);
/**
* Render and return an array structure suitable for generating HTML
* based differences. Generally called by subclasses that generate a
* HTML based diff and return an array of the changes to show in the diff.
*
* @return array An array of the generated chances, suitable for presentation in HTML.
*/
public function render()
{
// As we'll be modifying a & b to include our change markers,
// we need to get the contents and store them here. That way
// we're not going to destroy the original data
$a = $this->diff->getA();
$b = $this->diff->getB();
$changes = array();
$opCodes = $this->diff->getGroupedOpcodes();
foreach($opCodes as $group) {
$blocks = array();
$lastTag = null;
$lastBlock = 0;
foreach($group as $code) {
list($tag, $i1, $i2, $j1, $j2) = $code;
if($tag == 'replace' && $i2 - $i1 == $j2 - $j1) {
for($i = 0; $i < ($i2 - $i1); ++$i) {
$fromLine = $a[$i1 + $i];
$toLine = $b[$j1 + $i];
list($start, $end) = $this->getChangeExtent($fromLine, $toLine);
if($start != 0 || $end != 0) {
$realEnd = mb_strlen($fromLine) + $end;
$fromLine = mb_substr($fromLine, 0, $start)
. "\0"
. mb_substr($fromLine, $start, $realEnd - $start)
. "\1"
. mb_substr($fromLine, $realEnd);
$realEnd = mb_strlen($toLine) + $end;
$toLine = mb_substr($toLine, 0, $start)
. "\0"
. mb_substr($toLine, $start, $realEnd - $start)
. "\1"
. mb_substr($toLine, $realEnd);
$a[$i1 + $i] = $fromLine;
$b[$j1 + $i] = $toLine;
}
}
}
if($tag != $lastTag) {
$blocks[] = array(
'tag' => $tag,
'base' => array(
'offset' => $i1,
'lines' => array()
),
'changed' => array(
'offset' => $j1,
'lines' => array()
)
);
$lastBlock = count($blocks)-1;
}
$lastTag = $tag;
if($tag == 'equal') {
$lines = array_slice($a, $i1, ($i2 - $i1));
$blocks[$lastBlock]['base']['lines'] += $this->formatLines($lines);
$lines = array_slice($b, $j1, ($j2 - $j1));
$blocks[$lastBlock]['changed']['lines'] += $this->formatLines($lines);
}
else {
if($tag == 'replace' || $tag == 'delete') {
$lines = array_slice($a, $i1, ($i2 - $i1));
$lines = $this->formatLines($lines);
$lines = str_replace(array("\0", "\1"), array('<del>', '</del>'), $lines);
$blocks[$lastBlock]['base']['lines'] += $lines;
}
if($tag == 'replace' || $tag == 'insert') {
$lines = array_slice($b, $j1, ($j2 - $j1));
$lines = $this->formatLines($lines);
$lines = str_replace(array("\0", "\1"), array('<ins>', '</ins>'), $lines);
$blocks[$lastBlock]['changed']['lines'] += $lines;
}
}
}
$changes[] = $blocks;
}
return $changes;
}
/**
* Given two strings, determine where the changes in the two strings
* begin, and where the changes in the two strings end.
*
* @param string $fromLine The first string.
* @param string $toLine The second string.
* @return array Array containing the starting position (0 by default) and the ending position (-1 by default)
*/
private function getChangeExtent($fromLine, $toLine)
{
$start = 0;
$limit = min(mb_strlen($fromLine), mb_strlen($toLine));
while($start < $limit && mb_substr($fromLine, $start, 1) == mb_substr($toLine, $start, 1)) {
++$start;
}
$end = -1;
$limit = $limit - $start;
while(-$end <= $limit && mb_substr($fromLine, $end, 1) == mb_substr($toLine, $end, 1)) {
--$end;
}
return array(
$start,
$end + 1
);
}
/**
* Format a series of lines suitable for output in a HTML rendered diff.
* This involves replacing tab characters with spaces, making the HTML safe
* for output, ensuring that double spaces are replaced with &nbsp; etc.
*
* @param array $lines Array of lines to format.
* @return array Array of the formatted lines.
*/
private function formatLines($lines)
{
if ($this->options['tabSize'] !== false) {
$lines = array_map(array($this, 'ExpandTabs'), $lines);
}
$lines = array_map(array($this, 'HtmlSafe'), $lines);
foreach($lines as &$line) {
$line = preg_replace_callback('# ( +)|^ #', __CLASS__."::fixSpaces", $line);
}
return $lines;
}
/**
* Replace a string containing spaces with a HTML representation using &nbsp;.
*
* @param string $matches Regex matches array.
* @return string The HTML representation of the string.
*/
public static function fixSpaces($matches)
{
$spaces = isset($matches[1]) ? $matches[1] : '';
$count = strlen($spaces);
if($count == 0) {
return '';
}
$div = floor($count / 2);
$mod = $count % 2;
return str_repeat('&nbsp; ', $div).str_repeat('&nbsp;', $mod);
}
/**
* Replace tabs in a single line with a number of spaces as defined by the tabSize option.
*
* @param string $line The containing tabs to convert.
* @return string The line with the tabs converted to spaces.
*/
private function expandTabs($line)
{
return str_replace("\t", str_repeat(' ', $this->options['tabSize']), $line);
}
/**
* Make a string containing HTML safe for output on a page.
*
* @param string $string The string.
* @return string The string with the HTML characters replaced by entities.
*/
private function htmlSafe($string)
{
return htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8');
}
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* Inline HTML diff generator for PHP DiffLib.
*
* PHP version 5
*
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the Chris Boulton nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package DiffLib
* @author Chris Boulton <chris.boulton@interspire.com>
* @copyright (c) 2009 Chris Boulton
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
* @version 1.1
* @link http://github.com/chrisboulton/php-diff
*/
require_once dirname(__FILE__).'/Array.php';
class Diff_Renderer_Html_Inline extends Diff_Renderer_Html_Array
{
/**
* Render a and return diff with changes between the two sequences
* displayed inline (under each other)
*
* @return string The generated inline diff.
*/
public function render()
{
$changes = parent::render();
$html = '';
if(empty($changes)) {
return $html;
}
$html .= '<table class="Differences DifferencesInline">';
$html .= '<thead>';
$html .= '<tr>';
$html .= '<th>Old</th>';
$html .= '<th>New</th>';
$html .= '<th>Differences</th>';
$html .= '</tr>';
$html .= '</thead>';
foreach($changes as $i => $blocks) {
// If this is a separate block, we're condensing code so output ...,
// indicating a significant portion of the code has been collapsed as
// it is the same
if($i > 0) {
$html .= '<tbody class="Skipped">';
$html .= '<th>&hellip;</th>';
$html .= '<th>&hellip;</th>';
$html .= '<td>&nbsp;</td>';
$html .= '</tbody>';
}
foreach($blocks as $change) {
$html .= '<tbody class="Change'.ucfirst($change['tag']).'">';
// Equal changes should be shown on both sides of the diff
if($change['tag'] == 'equal') {
foreach($change['base']['lines'] as $no => $line) {
$fromLine = $change['base']['offset'] + $no + 1;
$toLine = $change['changed']['offset'] + $no + 1;
$html .= '<tr>';
$html .= '<th>'.$fromLine.'</th>';
$html .= '<th>'.$toLine.'</th>';
$html .= '<td class="Left">'.$line.'</td>';
$html .= '</tr>';
}
}
// Added lines only on the right side
else if($change['tag'] == 'insert') {
foreach($change['changed']['lines'] as $no => $line) {
$toLine = $change['changed']['offset'] + $no + 1;
$html .= '<tr>';
$html .= '<th>&nbsp;</th>';
$html .= '<th>'.$toLine.'</th>';
$html .= '<td class="Right"><ins>'.$line.'</ins>&nbsp;</td>';
$html .= '</tr>';
}
}
// Show deleted lines only on the left side
else if($change['tag'] == 'delete') {
foreach($change['base']['lines'] as $no => $line) {
$fromLine = $change['base']['offset'] + $no + 1;
$html .= '<tr>';
$html .= '<th>'.$fromLine.'</th>';
$html .= '<th>&nbsp;</th>';
$html .= '<td class="Left"><del>'.$line.'</del>&nbsp;</td>';
$html .= '</tr>';
}
}
// Show modified lines on both sides
else if($change['tag'] == 'replace') {
foreach($change['base']['lines'] as $no => $line) {
$fromLine = $change['base']['offset'] + $no + 1;
$html .= '<tr>';
$html .= '<th>'.$fromLine.'</th>';
$html .= '<th>&nbsp;</th>';
$html .= '<td class="Left"><span>'.$line.'</span></td>';
$html .= '</tr>';
}
foreach($change['changed']['lines'] as $no => $line) {
$toLine = $change['changed']['offset'] + $no + 1;
$html .= '<tr>';
$html .= '<th>&nbsp;</th>';
$html .= '<th>'.$toLine.'</th>';
$html .= '<td class="Right"><span>'.$line.'</span></td>';
$html .= '</tr>';
}
}
$html .= '</tbody>';
}
}
$html .= '</table>';
return $html;
}
}

View File

@@ -0,0 +1,163 @@
<?php
/**
* Side by Side HTML diff generator for PHP DiffLib.
*
* PHP version 5
*
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the Chris Boulton nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package DiffLib
* @author Chris Boulton <chris.boulton@interspire.com>
* @copyright (c) 2009 Chris Boulton
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
* @version 1.1
* @link http://github.com/chrisboulton/php-diff
*/
require_once dirname(__FILE__).'/Array.php';
class Diff_Renderer_Html_SideBySide extends Diff_Renderer_Html_Array
{
/**
* Render a and return diff with changes between the two sequences
* displayed side by side.
*
* @return string The generated side by side diff.
*/
public function render()
{
$changes = parent::render();
$html = '';
if(empty($changes)) {
return $html;
}
$html .= '<table class="Differences DifferencesSideBySide">';
$html .= '<thead>';
$html .= '<tr>';
$html .= '<th colspan="2">Old Version</th>';
$html .= '<th colspan="2">New Version</th>';
$html .= '</tr>';
$html .= '</thead>';
foreach($changes as $i => $blocks) {
if($i > 0) {
$html .= '<tbody class="Skipped">';
$html .= '<th>&hellip;</th><td>&nbsp;</td>';
$html .= '<th>&hellip;</th><td>&nbsp;</td>';
$html .= '</tbody>';
}
foreach($blocks as $change) {
$html .= '<tbody class="Change'.ucfirst($change['tag']).'">';
// Equal changes should be shown on both sides of the diff
if($change['tag'] == 'equal') {
foreach($change['base']['lines'] as $no => $line) {
$fromLine = $change['base']['offset'] + $no + 1;
$toLine = $change['changed']['offset'] + $no + 1;
$html .= '<tr>';
$html .= '<th>'.$fromLine.'</th>';
$html .= '<td class="Left"><span>'.$line.'</span>&nbsp;</td>';
$html .= '<th>'.$toLine.'</th>';
$html .= '<td class="Right"><span>'.$line.'</span>&nbsp;</td>';
$html .= '</tr>';
}
}
// Added lines only on the right side
else if($change['tag'] == 'insert') {
foreach($change['changed']['lines'] as $no => $line) {
$toLine = $change['changed']['offset'] + $no + 1;
$html .= '<tr>';
$html .= '<th>&nbsp;</th>';
$html .= '<td class="Left">&nbsp;</td>';
$html .= '<th>'.$toLine.'</th>';
$html .= '<td class="Right"><ins>'.$line.'</ins>&nbsp;</td>';
$html .= '</tr>';
}
}
// Show deleted lines only on the left side
else if($change['tag'] == 'delete') {
foreach($change['base']['lines'] as $no => $line) {
$fromLine = $change['base']['offset'] + $no + 1;
$html .= '<tr>';
$html .= '<th>'.$fromLine.'</th>';
$html .= '<td class="Left"><del>'.$line.'</del>&nbsp;</td>';
$html .= '<th>&nbsp;</th>';
$html .= '<td class="Right">&nbsp;</td>';
$html .= '</tr>';
}
}
// Show modified lines on both sides
else if($change['tag'] == 'replace') {
if(count($change['base']['lines']) >= count($change['changed']['lines'])) {
foreach($change['base']['lines'] as $no => $line) {
$fromLine = $change['base']['offset'] + $no + 1;
$html .= '<tr>';
$html .= '<th>'.$fromLine.'</th>';
$html .= '<td class="Left"><span>'.$line.'</span>&nbsp;</td>';
if(!isset($change['changed']['lines'][$no])) {
$toLine = '&nbsp;';
$changedLine = '&nbsp;';
}
else {
$toLine = $change['base']['offset'] + $no + 1;
$changedLine = '<span>'.$change['changed']['lines'][$no].'</span>';
}
$html .= '<th>'.$toLine.'</th>';
$html .= '<td class="Right">'.$changedLine.'</td>';
$html .= '</tr>';
}
}
else {
foreach($change['changed']['lines'] as $no => $changedLine) {
if(!isset($change['base']['lines'][$no])) {
$fromLine = '&nbsp;';
$line = '&nbsp;';
}
else {
$fromLine = $change['base']['offset'] + $no + 1;
$line = '<span>'.$change['base']['lines'][$no].'</span>';
}
$html .= '<tr>';
$html .= '<th>'.$fromLine.'</th>';
$html .= '<td class="Left"><span>'.$line.'</span>&nbsp;</td>';
$toLine = $change['changed']['offset'] + $no + 1;
$html .= '<th>'.$toLine.'</th>';
$html .= '<td class="Right">'.$changedLine.'</td>';
$html .= '</tr>';
}
}
}
$html .= '</tbody>';
}
}
$html .= '</table>';
return $html;
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* Context diff generator for PHP DiffLib.
*
* PHP version 5
*
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the Chris Boulton nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package DiffLib
* @author Chris Boulton <chris.boulton@interspire.com>
* @copyright (c) 2009 Chris Boulton
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
* @version 1.1
* @link http://github.com/chrisboulton/php-diff
*/
require_once dirname(__FILE__).'/../Abstract.php';
class Diff_Renderer_Text_Context extends Diff_Renderer_Abstract
{
/**
* @var array Array of the different opcode tags and how they map to the context diff equivalent.
*/
private $tagMap = array(
'insert' => '+',
'delete' => '-',
'replace' => '!',
'equal' => ' '
);
/**
* Render and return a context formatted (old school!) diff file.
*
* @return string The generated context diff.
*/
public function render()
{
$diff = '';
$opCodes = $this->diff->getGroupedOpcodes();
foreach($opCodes as $group) {
$diff .= "***************\n";
$lastItem = count($group)-1;
$i1 = $group[0][1];
$i2 = $group[$lastItem][2];
$j1 = $group[0][3];
$j2 = $group[$lastItem][4];
if($i2 - $i1 >= 2) {
$diff .= '*** '.($group[0][1] + 1).','.$i2." ****".PHP_EOL;
}
else {
$diff .= '*** '.$i2." ****\n";
}
if($j2 - $j1 >= 2) {
$separator = '--- '.($j1 + 1).','.$j2." ----".PHP_EOL;
}
else {
$separator = '--- '.$j2." ----".PHP_EOL;
}
$hasVisible = false;
foreach($group as $code) {
if($code[0] == 'replace' || $code[0] == 'delete') {
$hasVisible = true;
break;
}
}
if($hasVisible) {
foreach($group as $code) {
list($tag, $i1, $i2, $j1, $j2) = $code;
if($tag == 'insert') {
continue;
}
$diff .= $this->tagMap[$tag].' '.implode(PHP_EOL.$this->tagMap[$tag].' ', $this->diff->GetA($i1, $i2)).PHP_EOL;
}
}
$hasVisible = false;
foreach($group as $code) {
if($code[0] == 'replace' || $code[0] == 'insert') {
$hasVisible = true;
break;
}
}
$diff .= $separator;
if($hasVisible) {
foreach($group as $code) {
list($tag, $i1, $i2, $j1, $j2) = $code;
if($tag == 'delete') {
continue;
}
$diff .= $this->tagMap[$tag].' '.implode(PHP_EOL.$this->tagMap[$tag].' ', $this->diff->GetB($j1, $j2)).PHP_EOL;
}
}
}
return $diff;
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* Unified diff generator for PHP DiffLib.
*
* PHP version 5
*
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the Chris Boulton nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package DiffLib
* @author Chris Boulton <chris.boulton@interspire.com>
* @copyright (c) 2009 Chris Boulton
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
* @version 1.1
* @link http://github.com/chrisboulton/php-diff
*/
require_once dirname(__FILE__).'/../Abstract.php';
class Diff_Renderer_Text_Unified extends Diff_Renderer_Abstract
{
/**
* Render and return a unified diff.
*
* @return string The unified diff.
*/
public function render()
{
$diff = '';
$opCodes = $this->diff->getGroupedOpcodes();
foreach($opCodes as $group) {
$lastItem = count($group)-1;
$i1 = $group[0][1];
$i2 = $group[$lastItem][2];
$j1 = $group[0][3];
$j2 = $group[$lastItem][4];
if($i1 == 0 && $i2 == 0) {
$i1 = -1;
$i2 = -1;
}
$diff .= '@@ -'.($i1 + 1).','.($i2 - $i1).' +'.($j1 + 1).','.($j2 - $j1)." @@".PHP_EOL;
foreach($group as $code) {
list($tag, $i1, $i2, $j1, $j2) = $code;
if($tag == 'equal') {
$diff .= ' '.implode(PHP_EOL." ", $this->diff->GetA($i1, $i2)).PHP_EOL;
}
else {
if($tag == 'replace' || $tag == 'delete') {
$diff .= '-'.implode(PHP_EOL."-", $this->diff->GetA($i1, $i2)).PHP_EOL;
}
if($tag == 'replace' || $tag == 'insert') {
$diff .= '+'.implode(PHP_EOL."+", $this->diff->GetB($j1, $j2)).PHP_EOL;
}
}
}
}
return $diff;
}
}

View File

@@ -0,0 +1,752 @@
<?php
/**
* Sequence matcher for Diff
*
* PHP version 5
*
* Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the Chris Boulton nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @package Diff
* @author Chris Boulton <chris.boulton@interspire.com>
* @copyright (c) 2009 Chris Boulton
* @license New BSD License http://www.opensource.org/licenses/bsd-license.php
* @version 1.1
* @link http://github.com/chrisboulton/php-diff
*/
class Diff_SequenceMatcher
{
/**
* @var string|array Either a string or an array containing a callback function to determine if a line is "junk" or not.
*/
private $junkCallback = null;
/**
* @var array The first sequence to compare against.
*/
private $a = null;
/**
* @var array The second sequence.
*/
private $b = null;
/**
* @var array Array of characters that are considered junk from the second sequence. Characters are the array key.
*/
private $junkDict = array();
/**
* @var array Array of indices that do not contain junk elements.
*/
private $b2j = array();
private $options = array();
private $matchingBlocks = null;
private $opCodes = null;
private $fullBCount = null;
private $defaultOptions = array(
'ignoreNewLines' => false,
'ignoreWhitespace' => false,
'ignoreCase' => false
);
/**
* The constructor. With the sequences being passed, they'll be set for the
* sequence matcher and it will perform a basic cleanup & calculate junk
* elements.
*
* @param string|array $a A string or array containing the lines to compare against.
* @param string|array $b A string or array containing the lines to compare.
* @param string|array $junkCallback Either an array or string that references a callback function (if there is one) to determine 'junk' characters.
* @param array $options
*/
public function __construct($a, $b, $junkCallback=null, $options)
{
$this->a = null;
$this->b = null;
$this->junkCallback = $junkCallback;
$this->setOptions($options);
$this->setSequences($a, $b);
}
/**
* Set new options
*
* @param array $options
*/
public function setOptions($options)
{
$this->options = array_merge($this->defaultOptions, $options);
}
/**
* Set the first and second sequences to use with the sequence matcher.
*
* @param string|array $a A string or array containing the lines to compare against.
* @param string|array $b A string or array containing the lines to compare.
*/
public function setSequences($a, $b)
{
$this->setSeq1($a);
$this->setSeq2($b);
}
/**
* Set the first sequence ($a) and reset any internal caches to indicate that
* when calling the calculation methods, we need to recalculate them.
*
* @param string|array $a The sequence to set as the first sequence.
*/
public function setSeq1($a)
{
if(!is_array($a)) {
$a = str_split($a);
}
if($a == $this->a) {
return;
}
$this->a= $a;
$this->matchingBlocks = null;
$this->opCodes = null;
}
/**
* Set the second sequence ($b) and reset any internal caches to indicate that
* when calling the calculation methods, we need to recalculate them.
*
* @param string|array $b The sequence to set as the second sequence.
*/
public function setSeq2($b)
{
if(!is_array($b)) {
$b = str_split($b);
}
if($b == $this->b) {
return;
}
$this->b = $b;
$this->matchingBlocks = null;
$this->opCodes = null;
$this->fullBCount = null;
$this->chainB();
}
/**
* Generate the internal arrays containing the list of junk and non-junk
* characters for the second ($b) sequence.
*/
private function chainB()
{
$length = count ($this->b);
$this->b2j = array();
$popularDict = array();
for($i = 0; $i < $length; ++$i) {
$char = $this->b[$i];
if(isset($this->b2j[$char])) {
if($length >= 200 && count($this->b2j[$char]) * 100 > $length) {
$popularDict[$char] = 1;
unset($this->b2j[$char]);
}
else {
$this->b2j[$char][] = $i;
}
}
else {
$this->b2j[$char] = array(
$i
);
}
}
// Remove leftovers
foreach(array_keys($popularDict) as $char) {
unset($this->b2j[$char]);
}
$this->junkDict = array();
if(is_callable($this->junkCallback)) {
foreach(array_keys($popularDict) as $char) {
if(call_user_func($this->junkCallback, $char)) {
$this->junkDict[$char] = 1;
unset($popularDict[$char]);
}
}
foreach(array_keys($this->b2j) as $char) {
if(call_user_func($this->junkCallback, $char)) {
$this->junkDict[$char] = 1;
unset($this->b2j[$char]);
}
}
}
}
/**
* Checks if a particular character is in the junk dictionary
* for the list of junk characters.
* @param $b
* @return boolean True if the character is considered junk. False if not.
*/
private function isBJunk($b)
{
if(isset($this->junkDict[$b])) {
return true;
}
return false;
}
/**
* Find the longest matching block in the two sequences, as defined by the
* lower and upper constraints for each sequence. (for the first sequence,
* $alo - $ahi and for the second sequence, $blo - $bhi)
*
* Essentially, of all of the maximal matching blocks, return the one that
* startest earliest in $a, and all of those maximal matching blocks that
* start earliest in $a, return the one that starts earliest in $b.
*
* If the junk callback is defined, do the above but with the restriction
* that the junk element appears in the block. Extend it as far as possible
* by matching only junk elements in both $a and $b.
*
* @param int $alo The lower constraint for the first sequence.
* @param int $ahi The upper constraint for the first sequence.
* @param int $blo The lower constraint for the second sequence.
* @param int $bhi The upper constraint for the second sequence.
* @return array Array containing the longest match that includes the starting position in $a, start in $b and the length/size.
*/
public function findLongestMatch($alo, $ahi, $blo, $bhi)
{
$a = $this->a;
$b = $this->b;
$bestI = $alo;
$bestJ = $blo;
$bestSize = 0;
$j2Len = array();
$nothing = array();
for($i = $alo; $i < $ahi; ++$i) {
$newJ2Len = array();
$jDict = $this->arrayGetDefault($this->b2j, $a[$i], $nothing);
foreach($jDict as $j) {
if($j < $blo) {
continue;
}
else if($j >= $bhi) {
break;
}
$k = $this->arrayGetDefault($j2Len, $j -1, 0) + 1;
$newJ2Len[$j] = $k;
if($k > $bestSize) {
$bestI = $i - $k + 1;
$bestJ = $j - $k + 1;
$bestSize = $k;
}
}
$j2Len = $newJ2Len;
}
while($bestI > $alo && $bestJ > $blo && !$this->isBJunk($b[$bestJ - 1]) &&
!$this->linesAreDifferent($bestI - 1, $bestJ - 1)) {
--$bestI;
--$bestJ;
++$bestSize;
}
while($bestI + $bestSize < $ahi && ($bestJ + $bestSize) < $bhi &&
!$this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) {
++$bestSize;
}
while($bestI > $alo && $bestJ > $blo && $this->isBJunk($b[$bestJ - 1]) &&
!$this->linesAreDifferent($bestI - 1, $bestJ - 1)) {
--$bestI;
--$bestJ;
++$bestSize;
}
while($bestI + $bestSize < $ahi && $bestJ + $bestSize < $bhi &&
$this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) {
++$bestSize;
}
return array(
$bestI,
$bestJ,
$bestSize
);
}
/**
* Check if the two lines at the given indexes are different or not.
*
* @param int $aIndex Line number to check against in a.
* @param int $bIndex Line number to check against in b.
* @return boolean True if the lines are different and false if not.
*/
public function linesAreDifferent($aIndex, $bIndex)
{
$lineA = $this->a[$aIndex];
$lineB = $this->b[$bIndex];
if($this->options['ignoreWhitespace']) {
$replace = array("\t", ' ');
$lineA = str_replace($replace, '', $lineA);
$lineB = str_replace($replace, '', $lineB);
}
if($this->options['ignoreCase']) {
$lineA = strtolower($lineA);
$lineB = strtolower($lineB);
}
if($lineA != $lineB) {
return true;
}
return false;
}
/**
* Return a nested set of arrays for all of the matching sub-sequences
* in the strings $a and $b.
*
* Each block contains the lower constraint of the block in $a, the lower
* constraint of the block in $b and finally the number of lines that the
* block continues for.
*
* @return array Nested array of the matching blocks, as described by the function.
*/
public function getMatchingBlocks()
{
if(!empty($this->matchingBlocks)) {
return $this->matchingBlocks;
}
$aLength = count($this->a);
$bLength = count($this->b);
$queue = array(
array(
0,
$aLength,
0,
$bLength
)
);
$matchingBlocks = array();
while(!empty($queue)) {
list($alo, $ahi, $blo, $bhi) = array_pop($queue);
$x = $this->findLongestMatch($alo, $ahi, $blo, $bhi);
list($i, $j, $k) = $x;
if($k) {
$matchingBlocks[] = $x;
if($alo < $i && $blo < $j) {
$queue[] = array(
$alo,
$i,
$blo,
$j
);
}
if($i + $k < $ahi && $j + $k < $bhi) {
$queue[] = array(
$i + $k,
$ahi,
$j + $k,
$bhi
);
}
}
}
usort($matchingBlocks, array($this, 'tupleSort'));
$i1 = 0;
$j1 = 0;
$k1 = 0;
$nonAdjacent = array();
foreach($matchingBlocks as $block) {
list($i2, $j2, $k2) = $block;
if($i1 + $k1 == $i2 && $j1 + $k1 == $j2) {
$k1 += $k2;
}
else {
if($k1) {
$nonAdjacent[] = array(
$i1,
$j1,
$k1
);
}
$i1 = $i2;
$j1 = $j2;
$k1 = $k2;
}
}
if($k1) {
$nonAdjacent[] = array(
$i1,
$j1,
$k1
);
}
$nonAdjacent[] = array(
$aLength,
$bLength,
0
);
$this->matchingBlocks = $nonAdjacent;
return $this->matchingBlocks;
}
/**
* Return a list of all of the opcodes for the differences between the
* two strings.
*
* The nested array returned contains an array describing the opcode
* which includes:
* 0 - The type of tag (as described below) for the opcode.
* 1 - The beginning line in the first sequence.
* 2 - The end line in the first sequence.
* 3 - The beginning line in the second sequence.
* 4 - The end line in the second sequence.
*
* The different types of tags include:
* replace - The string from $i1 to $i2 in $a should be replaced by
* the string in $b from $j1 to $j2.
* delete - The string in $a from $i1 to $j2 should be deleted.
* insert - The string in $b from $j1 to $j2 should be inserted at
* $i1 in $a.
* equal - The two strings with the specified ranges are equal.
*
* @return array Array of the opcodes describing the differences between the strings.
*/
public function getOpCodes()
{
if(!empty($this->opCodes)) {
return $this->opCodes;
}
$i = 0;
$j = 0;
$this->opCodes = array();
$blocks = $this->getMatchingBlocks();
foreach($blocks as $block) {
list($ai, $bj, $size) = $block;
$tag = '';
if($i < $ai && $j < $bj) {
$tag = 'replace';
}
else if($i < $ai) {
$tag = 'delete';
}
else if($j < $bj) {
$tag = 'insert';
}
if($tag) {
$this->opCodes[] = array(
$tag,
$i,
$ai,
$j,
$bj
);
}
$i = $ai + $size;
$j = $bj + $size;
if($size) {
$this->opCodes[] = array(
'equal',
$ai,
$i,
$bj,
$j
);
}
}
return $this->opCodes;
}
/**
* Return a series of nested arrays containing different groups of generated
* opcodes for the differences between the strings with up to $context lines
* of surrounding content.
*
* Essentially what happens here is any big equal blocks of strings are stripped
* out, the smaller subsets of changes are then arranged in to their groups.
* This means that the sequence matcher and diffs do not need to include the full
* content of the different files but can still provide context as to where the
* changes are.
*
* @param int $context The number of lines of context to provide around the groups.
* @return array Nested array of all of the grouped opcodes.
*/
public function getGroupedOpcodes($context=3)
{
$opCodes = $this->getOpCodes();
if(empty($opCodes)) {
$opCodes = array(
array(
'equal',
0,
1,
0,
1
)
);
}
if($opCodes[0][0] == 'equal') {
$opCodes[0] = array(
$opCodes[0][0],
max($opCodes[0][1], $opCodes[0][2] - $context),
$opCodes[0][2],
max($opCodes[0][3], $opCodes[0][4] - $context),
$opCodes[0][4]
);
}
$lastItem = count($opCodes) - 1;
if($opCodes[$lastItem][0] == 'equal') {
list($tag, $i1, $i2, $j1, $j2) = $opCodes[$lastItem];
$opCodes[$lastItem] = array(
$tag,
$i1,
min($i2, $i1 + $context),
$j1,
min($j2, $j1 + $context)
);
}
$maxRange = $context * 2;
$groups = array();
$group = array();
foreach($opCodes as $code) {
list($tag, $i1, $i2, $j1, $j2) = $code;
if($tag == 'equal' && $i2 - $i1 > $maxRange) {
$group[] = array(
$tag,
$i1,
min($i2, $i1 + $context),
$j1,
min($j2, $j1 + $context)
);
$groups[] = $group;
$group = array();
$i1 = max($i1, $i2 - $context);
$j1 = max($j1, $j2 - $context);
}
$group[] = array(
$tag,
$i1,
$i2,
$j1,
$j2
);
}
if(!empty($group) && !(count($group) == 1 && $group[0][0] == 'equal')) {
$groups[] = $group;
}
return $groups;
}
/**
* Return a measure of the similarity between the two sequences.
* This will be a float value between 0 and 1.
*
* Out of all of the ratio calculation functions, this is the most
* expensive to call if getMatchingBlocks or getOpCodes is yet to be
* called. The other calculation methods (quickRatio and realquickRatio)
* can be used to perform quicker calculations but may be less accurate.
*
* The ratio is calculated as (2 * number of matches) / total number of
* elements in both sequences.
*
* @return float The calculated ratio.
*/
public function Ratio()
{
$matches = array_reduce($this->getMatchingBlocks(), array($this, 'ratioReduce'), 0);
return $this->calculateRatio($matches, count ($this->a) + count ($this->b));
}
/**
* Helper function to calculate the number of matches for Ratio().
*
* @param int $sum The running total for the number of matches.
* @param array $triple Array containing the matching block triple to add to the running total.
* @return int The new running total for the number of matches.
*/
private function ratioReduce($sum, $triple)
{
return $sum + ($triple[count($triple) - 1]);
}
/**
* Quickly return an upper bound ratio for the similarity of the strings.
* This is quicker to compute than Ratio().
*
* @return float The calculated ratio.
*/
private function quickRatio()
{
if($this->fullBCount === null) {
$this->fullBCount = array();
$bLength = count ($this->b);
for($i = 0; $i < $bLength; ++$i) {
$char = $this->b[$i];
$this->fullBCount[$char] = $this->arrayGetDefault($this->fullBCount, $char, 0) + 1;
}
}
$avail = array();
$matches = 0;
$aLength = count ($this->a);
for($i = 0; $i < $aLength; ++$i) {
$char = $this->a[$i];
if(isset($avail[$char])) {
$numb = $avail[$char];
}
else {
$numb = $this->arrayGetDefault($this->fullBCount, $char, 0);
}
$avail[$char] = $numb - 1;
if($numb > 0) {
++$matches;
}
}
$this->calculateRatio($matches, count ($this->a) + count ($this->b));
}
/**
* Return an upper bound ratio really quickly for the similarity of the strings.
* This is quicker to compute than Ratio() and quickRatio().
*
* @return float The calculated ratio.
*/
private function realquickRatio()
{
$aLength = count ($this->a);
$bLength = count ($this->b);
return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength);
}
/**
* Helper function for calculating the ratio to measure similarity for the strings.
* The ratio is defined as being 2 * (number of matches / total length)
*
* @param int $matches The number of matches in the two strings.
* @param int $length The length of the two strings.
* @return float The calculated ratio.
*/
private function calculateRatio($matches, $length=0)
{
if($length) {
return 2 * ($matches / $length);
}
else {
return 1;
}
}
/**
* Helper function that provides the ability to return the value for a key
* in an array of it exists, or if it doesn't then return a default value.
* Essentially cleaner than doing a series of if(isset()) {} else {} calls.
*
* @param array $array The array to search.
* @param string $key The key to check that exists.
* @param mixed $default The value to return as the default value if the key doesn't exist.
* @return mixed The value from the array if the key exists or otherwise the default.
*/
private function arrayGetDefault($array, $key, $default)
{
if(isset($array[$key])) {
return $array[$key];
}
else {
return $default;
}
}
/**
* Sort an array by the nested arrays it contains. Helper function for getMatchingBlocks
*
* @param array $a First array to compare.
* @param array $b Second array to compare.
* @return int -1, 0 or 1, as expected by the usort function.
*/
private function tupleSort($a, $b)
{
$max = max(count($a), count($b));
for($i = 0; $i < $max; ++$i) {
if($a[$i] < $b[$i]) {
return -1;
}
else if($a[$i] > $b[$i]) {
return 1;
}
}
if(count($a) == count($b)) {
return 0;
}
else if(count($a) < count($b)) {
return -1;
}
else {
return 1;
}
}
}

213
vendor/phpspec/prophecy/CHANGES.md vendored Normal file
View File

@@ -0,0 +1,213 @@
1.8.0 / 2018/08/05
==================
* Support for void return types without explicit will (@crellbar)
* Clearer error message for unexpected method calls (@meridius)
* Clearer error message for aggregate exceptions (@meridius)
* More verbose `shouldBeCalledOnce` expectation (@olvlvl)
* Ability to double Throwable, or methods that extend it (@ciaranmcnulty)
* [fixed] Doubling methods where class has additional arguments to interface (@webimpress)
* [fixed] Doubling methods where arguments are nullable but default is not null (@webimpress)
* [fixed] Doubling magic methods on parent class (@dsnopek)
* [fixed] Check method predictions only once (@dontub)
* [fixed] Argument::containingString throwing error when called with non-string (@dcabrejas)
1.7.6 / 2018/04/18
==================
* Allow sebastian/comparator ^3.0 (@sebastianbergmann)
1.7.5 / 2018/02/11
==================
* Support for object return type hints (thanks @greg0ire)
1.7.4 / 2018/02/11
==================
* Fix issues with PHP 7.2 (thanks @greg0ire)
* Support object type hints in PHP 7.2 (thanks @@jansvoboda11)
1.7.3 / 2017/11/24
==================
* Fix SplInfo ClassPatch to work with Symfony 4 (Thanks @gnugat)
1.7.2 / 2017-10-04
==================
* Reverted "check method predictions only once" due to it breaking Spies
1.7.1 / 2017-10-03
==================
* Allow PHP5 keywords methods generation on PHP7 (thanks @bycosta)
* Allow reflection-docblock v4 (thanks @GrahamCampbell)
* Check method predictions only once (thanks @dontub)
* Escape file path sent to \SplFileObjectConstructor when running on Windows (thanks @danmartin-epiphany)
1.7.0 / 2017-03-02
==================
* Add full PHP 7.1 Support (thanks @prolic)
* Allow `sebastian/comparator ^2.0` (thanks @sebastianbergmann)
* Allow `sebastian/recursion-context ^3.0` (thanks @sebastianbergmann)
* Allow `\Error` instances in `ThrowPromise` (thanks @jameshalsall)
* Support `phpspec/phpspect ^3.2` (thanks @Sam-Burns)
* Fix failing builds (thanks @Sam-Burns)
1.6.2 / 2016-11-21
==================
* Added support for detecting @method on interfaces that the class itself implements, or when the stubbed class is an interface itself (thanks @Seldaek)
* Added support for sebastian/recursion-context 2 (thanks @sebastianbergmann)
* Added testing on PHP 7.1 on Travis (thanks @danizord)
* Fixed the usage of the phpunit comparator (thanks @Anyqax)
1.6.1 / 2016-06-07
==================
* Ignored empty method names in invalid `@method` phpdoc
* Fixed the mocking of SplFileObject
* Added compatibility with phpdocumentor/reflection-docblock 3
1.6.0 / 2016-02-15
==================
* Add Variadics support (thanks @pamil)
* Add ProphecyComparator for comparing objects that need revealing (thanks @jon-acker)
* Add ApproximateValueToken (thanks @dantleech)
* Add support for 'self' and 'parent' return type (thanks @bendavies)
* Add __invoke to allowed reflectable methods list (thanks @ftrrtf)
* Updated ExportUtil to reflect the latest changes by Sebastian (thanks @jakari)
* Specify the required php version for composer (thanks @jakzal)
* Exclude 'args' in the generated backtrace (thanks @oradwell)
* Fix code generation for scalar parameters (thanks @trowski)
* Fix missing sprintf in InvalidArgumentException __construct call (thanks @emmanuelballery)
* Fix phpdoc for magic methods (thanks @Tobion)
* Fix PhpDoc for interfaces usage (thanks @ImmRanneft)
* Prevent final methods from being manually extended (thanks @kamioftea)
* Enhance exception for invalid argument to ThrowPromise (thanks @Tobion)
1.5.0 / 2015-04-27
==================
* Add support for PHP7 scalar type hints (thanks @trowski)
* Add support for PHP7 return types (thanks @trowski)
* Update internal test suite to support PHP7
1.4.1 / 2015-04-27
==================
* Fixed bug in closure-based argument tokens (#181)
1.4.0 / 2015-03-27
==================
* Fixed errors in return type phpdocs (thanks @sobit)
* Fixed stringifying of hash containing one value (thanks @avant1)
* Improved clarity of method call expectation exception (thanks @dantleech)
* Add ability to specify which argument is returned in willReturnArgument (thanks @coderbyheart)
* Add more information to MethodNotFound exceptions (thanks @ciaranmcnulty)
* Support for mocking classes with methods that return references (thanks @edsonmedina)
* Improved object comparison (thanks @whatthejeff)
* Adopted '^' in composer dependencies (thanks @GrahamCampbell)
* Fixed non-typehinted arguments being treated as optional (thanks @whatthejeff)
* Magic methods are now filtered for keywords (thanks @seagoj)
* More readable errors for failure when expecting single calls (thanks @dantleech)
1.3.1 / 2014-11-17
==================
* Fix the edge case when failed predictions weren't recorded for `getCheckedPredictions()`
1.3.0 / 2014-11-14
==================
* Add a way to get checked predictions with `MethodProphecy::getCheckedPredictions()`
* Fix HHVM compatibility
* Remove dead code (thanks @stof)
* Add support for DirectoryIterators (thanks @shanethehat)
1.2.0 / 2014-07-18
==================
* Added support for doubling magic methods documented in the class phpdoc (thanks @armetiz)
* Fixed a segfault appearing in some cases (thanks @dmoreaulf)
* Fixed the doubling of methods with typehints on non-existent classes (thanks @gquemener)
* Added support for internal classes using keywords as method names (thanks @milan)
* Added IdenticalValueToken and Argument::is (thanks @florianv)
* Removed the usage of scalar typehints in HHVM as HHVM 3 does not support them anymore in PHP code (thanks @whatthejeff)
1.1.2 / 2014-01-24
==================
* Spy automatically promotes spied method call to an expected one
1.1.1 / 2014-01-15
==================
* Added support for HHVM
1.1.0 / 2014-01-01
==================
* Changed the generated class names to use a static counter instead of a random number
* Added a clss patch for ReflectionClass::newInstance to make its argument optional consistently (thanks @docteurklein)
* Fixed mirroring of classes with typehints on non-existent classes (thanks @docteurklein)
* Fixed the support of array callables in CallbackPromise and CallbackPrediction (thanks @ciaranmcnulty)
* Added support for properties in ObjectStateToken (thanks @adrienbrault)
* Added support for mocking classes with a final constructor (thanks @ciaranmcnulty)
* Added ArrayEveryEntryToken and Argument::withEveryEntry() (thanks @adrienbrault)
* Added an exception when trying to prophesize on a final method instead of ignoring silently (thanks @docteurklein)
* Added StringContainToken and Argument::containingString() (thanks @peterjmit)
* Added ``shouldNotHaveBeenCalled`` on the MethodProphecy (thanks @ciaranmcnulty)
* Fixed the comparison of objects in ExactValuetoken (thanks @sstok)
* Deprecated ``shouldNotBeenCalled`` in favor of ``shouldNotHaveBeenCalled``
1.0.4 / 2013-08-10
==================
* Better randomness for generated class names (thanks @sstok)
* Add support for interfaces into TypeToken and Argument::type() (thanks @sstok)
* Add support for old-style (method name === class name) constructors (thanks @l310 for report)
1.0.3 / 2013-07-04
==================
* Support callable typehints (thanks @stof)
* Do not attempt to autoload arrays when generating code (thanks @MarcoDeBortoli)
* New ArrayEntryToken (thanks @kagux)
1.0.2 / 2013-05-19
==================
* Logical `AND` token added (thanks @kagux)
* Logical `NOT` token added (thanks @kagux)
* Add support for setting custom constructor arguments
* Properly stringify hashes
* Record calls that throw exceptions
* Migrate spec suite to PhpSpec 2.0
1.0.1 / 2013-04-30
==================
* Fix broken UnexpectedCallException message
* Trim AggregateException message
1.0.0 / 2013-04-29
==================
* Improve exception messages
1.0.0-BETA2 / 2013-04-03
========================
* Add more debug information to CallTimes and Call prediction exception messages
* Fix MethodNotFoundException wrong namespace (thanks @gunnarlium)
* Fix some typos in the exception messages (thanks @pborreli)
1.0.0-BETA1 / 2013-03-25
========================
* Initial release

23
vendor/phpspec/prophecy/LICENSE vendored Normal file
View File

@@ -0,0 +1,23 @@
Copyright (c) 2013 Konstantin Kudryashov <ever.zet@gmail.com>
Marcello Duarte <marcello.duarte@gmail.com>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

391
vendor/phpspec/prophecy/README.md vendored Normal file
View File

@@ -0,0 +1,391 @@
# Prophecy
[![Stable release](https://poser.pugx.org/phpspec/prophecy/version.svg)](https://packagist.org/packages/phpspec/prophecy)
[![Build Status](https://travis-ci.org/phpspec/prophecy.svg?branch=master)](https://travis-ci.org/phpspec/prophecy)
Prophecy is a highly opinionated yet very powerful and flexible PHP object mocking
framework. Though initially it was created to fulfil phpspec2 needs, it is flexible
enough to be used inside any testing framework out there with minimal effort.
## A simple example
```php
<?php
class UserTest extends PHPUnit_Framework_TestCase
{
private $prophet;
public function testPasswordHashing()
{
$hasher = $this->prophet->prophesize('App\Security\Hasher');
$user = new App\Entity\User($hasher->reveal());
$hasher->generateHash($user, 'qwerty')->willReturn('hashed_pass');
$user->setPassword('qwerty');
$this->assertEquals('hashed_pass', $user->getPassword());
}
protected function setup()
{
$this->prophet = new \Prophecy\Prophet;
}
protected function tearDown()
{
$this->prophet->checkPredictions();
}
}
```
## Installation
### Prerequisites
Prophecy requires PHP 5.3.3 or greater.
### Setup through composer
First, add Prophecy to the list of dependencies inside your `composer.json`:
```json
{
"require-dev": {
"phpspec/prophecy": "~1.0"
}
}
```
Then simply install it with composer:
```bash
$> composer install --prefer-dist
```
You can read more about Composer on its [official webpage](http://getcomposer.org).
## How to use it
First of all, in Prophecy every word has a logical meaning, even the name of the library
itself (Prophecy). When you start feeling that, you'll become very fluid with this
tool.
For example, Prophecy has been named that way because it concentrates on describing the future
behavior of objects with very limited knowledge about them. But as with any other prophecy,
those object prophecies can't create themselves - there should be a Prophet:
```php
$prophet = new Prophecy\Prophet;
```
The Prophet creates prophecies by *prophesizing* them:
```php
$prophecy = $prophet->prophesize();
```
The result of the `prophesize()` method call is a new object of class `ObjectProphecy`. Yes,
that's your specific object prophecy, which describes how your object would behave
in the near future. But first, you need to specify which object you're talking about,
right?
```php
$prophecy->willExtend('stdClass');
$prophecy->willImplement('SessionHandlerInterface');
```
There are 2 interesting calls - `willExtend` and `willImplement`. The first one tells
object prophecy that our object should extend specific class, the second one says that
it should implement some interface. Obviously, objects in PHP can implement multiple
interfaces, but extend only one parent class.
### Dummies
Ok, now we have our object prophecy. What can we do with it? First of all, we can get
our object *dummy* by revealing its prophecy:
```php
$dummy = $prophecy->reveal();
```
The `$dummy` variable now holds a special dummy object. Dummy objects are objects that extend
and/or implement preset classes/interfaces by overriding all their public methods. The key
point about dummies is that they do not hold any logic - they just do nothing. Any method
of the dummy will always return `null` and the dummy will never throw any exceptions.
Dummy is your friend if you don't care about the actual behavior of this double and just need
a token object to satisfy a method typehint.
You need to understand one thing - a dummy is not a prophecy. Your object prophecy is still
assigned to `$prophecy` variable and in order to manipulate with your expectations, you
should work with it. `$dummy` is a dummy - a simple php object that tries to fulfil your
prophecy.
### Stubs
Ok, now we know how to create basic prophecies and reveal dummies from them. That's
awesome if we don't care about our _doubles_ (objects that reflect originals)
interactions. If we do, we need to use *stubs* or *mocks*.
A stub is an object double, which doesn't have any expectations about the object behavior,
but when put in specific environment, behaves in specific way. Ok, I know, it's cryptic,
but bear with me for a minute. Simply put, a stub is a dummy, which depending on the called
method signature does different things (has logic). To create stubs in Prophecy:
```php
$prophecy->read('123')->willReturn('value');
```
Oh wow. We've just made an arbitrary call on the object prophecy? Yes, we did. And this
call returned us a new object instance of class `MethodProphecy`. Yep, that's a specific
method with arguments prophecy. Method prophecies give you the ability to create method
promises or predictions. We'll talk about method predictions later in the _Mocks_ section.
#### Promises
Promises are logical blocks, that represent your fictional methods in prophecy terms
and they are handled by the `MethodProphecy::will(PromiseInterface $promise)` method.
As a matter of fact, the call that we made earlier (`willReturn('value')`) is a simple
shortcut to:
```php
$prophecy->read('123')->will(new Prophecy\Promise\ReturnPromise(array('value')));
```
This promise will cause any call to our double's `read()` method with exactly one
argument - `'123'` to always return `'value'`. But that's only for this
promise, there's plenty others you can use:
- `ReturnPromise` or `->willReturn(1)` - returns a value from a method call
- `ReturnArgumentPromise` or `->willReturnArgument($index)` - returns the nth method argument from call
- `ThrowPromise` or `->willThrow($exception)` - causes the method to throw specific exception
- `CallbackPromise` or `->will($callback)` - gives you a quick way to define your own custom logic
Keep in mind, that you can always add even more promises by implementing
`Prophecy\Promise\PromiseInterface`.
#### Method prophecies idempotency
Prophecy enforces same method prophecies and, as a consequence, same promises and
predictions for the same method calls with the same arguments. This means:
```php
$methodProphecy1 = $prophecy->read('123');
$methodProphecy2 = $prophecy->read('123');
$methodProphecy3 = $prophecy->read('321');
$methodProphecy1 === $methodProphecy2;
$methodProphecy1 !== $methodProphecy3;
```
That's interesting, right? Now you might ask me how would you define more complex
behaviors where some method call changes behavior of others. In PHPUnit or Mockery
you do that by predicting how many times your method will be called. In Prophecy,
you'll use promises for that:
```php
$user->getName()->willReturn(null);
// For PHP 5.4
$user->setName('everzet')->will(function () {
$this->getName()->willReturn('everzet');
});
// For PHP 5.3
$user->setName('everzet')->will(function ($args, $user) {
$user->getName()->willReturn('everzet');
});
// Or
$user->setName('everzet')->will(function ($args) use ($user) {
$user->getName()->willReturn('everzet');
});
```
And now it doesn't matter how many times or in which order your methods are called.
What matters is their behaviors and how well you faked it.
#### Arguments wildcarding
The previous example is awesome (at least I hope it is for you), but that's not
optimal enough. We hardcoded `'everzet'` in our expectation. Isn't there a better
way? In fact there is, but it involves understanding what this `'everzet'`
actually is.
You see, even if method arguments used during method prophecy creation look
like simple method arguments, in reality they are not. They are argument token
wildcards. As a matter of fact, `->setName('everzet')` looks like a simple call just
because Prophecy automatically transforms it under the hood into:
```php
$user->setName(new Prophecy\Argument\Token\ExactValueToken('everzet'));
```
Those argument tokens are simple PHP classes, that implement
`Prophecy\Argument\Token\TokenInterface` and tell Prophecy how to compare real arguments
with your expectations. And yes, those classnames are damn big. That's why there's a
shortcut class `Prophecy\Argument`, which you can use to create tokens like that:
```php
use Prophecy\Argument;
$user->setName(Argument::exact('everzet'));
```
`ExactValueToken` is not very useful in our case as it forced us to hardcode the username.
That's why Prophecy comes bundled with a bunch of other tokens:
- `IdenticalValueToken` or `Argument::is($value)` - checks that the argument is identical to a specific value
- `ExactValueToken` or `Argument::exact($value)` - checks that the argument matches a specific value
- `TypeToken` or `Argument::type($typeOrClass)` - checks that the argument matches a specific type or
classname
- `ObjectStateToken` or `Argument::which($method, $value)` - checks that the argument method returns
a specific value
- `CallbackToken` or `Argument::that(callback)` - checks that the argument matches a custom callback
- `AnyValueToken` or `Argument::any()` - matches any argument
- `AnyValuesToken` or `Argument::cetera()` - matches any arguments to the rest of the signature
- `StringContainsToken` or `Argument::containingString($value)` - checks that the argument contains a specific string value
And you can add even more by implementing `TokenInterface` with your own custom classes.
So, let's refactor our initial `{set,get}Name()` logic with argument tokens:
```php
use Prophecy\Argument;
$user->getName()->willReturn(null);
// For PHP 5.4
$user->setName(Argument::type('string'))->will(function ($args) {
$this->getName()->willReturn($args[0]);
});
// For PHP 5.3
$user->setName(Argument::type('string'))->will(function ($args, $user) {
$user->getName()->willReturn($args[0]);
});
// Or
$user->setName(Argument::type('string'))->will(function ($args) use ($user) {
$user->getName()->willReturn($args[0]);
});
```
That's it. Now our `{set,get}Name()` prophecy will work with any string argument provided to it.
We've just described how our stub object should behave, even though the original object could have
no behavior whatsoever.
One last bit about arguments now. You might ask, what happens in case of:
```php
use Prophecy\Argument;
$user->getName()->willReturn(null);
// For PHP 5.4
$user->setName(Argument::type('string'))->will(function ($args) {
$this->getName()->willReturn($args[0]);
});
// For PHP 5.3
$user->setName(Argument::type('string'))->will(function ($args, $user) {
$user->getName()->willReturn($args[0]);
});
// Or
$user->setName(Argument::type('string'))->will(function ($args) use ($user) {
$user->getName()->willReturn($args[0]);
});
$user->setName(Argument::any())->will(function () {
});
```
Nothing. Your stub will continue behaving the way it did before. That's because of how
arguments wildcarding works. Every argument token type has a different score level, which
wildcard then uses to calculate the final arguments match score and use the method prophecy
promise that has the highest score. In this case, `Argument::type()` in case of success
scores `5` and `Argument::any()` scores `3`. So the type token wins, as does the first
`setName()` method prophecy and its promise. The simple rule of thumb - more precise token
always wins.
#### Getting stub objects
Ok, now we know how to define our prophecy method promises, let's get our stub from
it:
```php
$stub = $prophecy->reveal();
```
As you might see, the only difference between how we get dummies and stubs is that with
stubs we describe every object conversation instead of just agreeing with `null` returns
(object being *dummy*). As a matter of fact, after you define your first promise
(method call), Prophecy will force you to define all the communications - it throws
the `UnexpectedCallException` for any call you didn't describe with object prophecy before
calling it on a stub.
### Mocks
Now we know how to define doubles without behavior (dummies) and doubles with behavior, but
no expectations (stubs). What's left is doubles for which we have some expectations. These
are called mocks and in Prophecy they look almost exactly the same as stubs, except that
they define *predictions* instead of *promises* on method prophecies:
```php
$entityManager->flush()->shouldBeCalled();
```
#### Predictions
The `shouldBeCalled()` method here assigns `CallPrediction` to our method prophecy.
Predictions are a delayed behavior check for your prophecies. You see, during the entire lifetime
of your doubles, Prophecy records every single call you're making against it inside your
code. After that, Prophecy can use this collected information to check if it matches defined
predictions. You can assign predictions to method prophecies using the
`MethodProphecy::should(PredictionInterface $prediction)` method. As a matter of fact,
the `shouldBeCalled()` method we used earlier is just a shortcut to:
```php
$entityManager->flush()->should(new Prophecy\Prediction\CallPrediction());
```
It checks if your method of interest (that matches both the method name and the arguments wildcard)
was called 1 or more times. If the prediction failed then it throws an exception. When does this
check happen? Whenever you call `checkPredictions()` on the main Prophet object:
```php
$prophet->checkPredictions();
```
In PHPUnit, you would want to put this call into the `tearDown()` method. If no predictions
are defined, it would do nothing. So it won't harm to call it after every test.
There are plenty more predictions you can play with:
- `CallPrediction` or `shouldBeCalled()` - checks that the method has been called 1 or more times
- `NoCallsPrediction` or `shouldNotBeCalled()` - checks that the method has not been called
- `CallTimesPrediction` or `shouldBeCalledTimes($count)` - checks that the method has been called
`$count` times
- `CallbackPrediction` or `should($callback)` - checks the method against your own custom callback
Of course, you can always create your own custom prediction any time by implementing
`PredictionInterface`.
### Spies
The last bit of awesomeness in Prophecy is out-of-the-box spies support. As I said in the previous
section, Prophecy records every call made during the double's entire lifetime. This means
you don't need to record predictions in order to check them. You can also do it
manually by using the `MethodProphecy::shouldHave(PredictionInterface $prediction)` method:
```php
$em = $prophet->prophesize('Doctrine\ORM\EntityManager');
$controller->createUser($em->reveal());
$em->flush()->shouldHaveBeenCalled();
```
Such manipulation with doubles is called spying. And with Prophecy it just works.

50
vendor/phpspec/prophecy/composer.json vendored Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "phpspec/prophecy",
"description": "Highly opinionated mocking framework for PHP 5.3+",
"keywords": ["Mock", "Stub", "Dummy", "Double", "Fake", "Spy"],
"homepage": "https://github.com/phpspec/prophecy",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
},
{
"name": "Marcello Duarte",
"email": "marcello.duarte@gmail.com"
}
],
"require": {
"php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
"sebastian/comparator": "^1.1|^2.0|^3.0",
"doctrine/instantiator": "^1.0.2",
"sebastian/recursion-context": "^1.0|^2.0|^3.0"
},
"require-dev": {
"phpspec/phpspec": "^2.5|^3.2",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
},
"autoload": {
"psr-0": {
"Prophecy\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Fixtures\\Prophecy\\": "fixtures"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.8.x-dev"
}
}
}

View File

@@ -0,0 +1,212 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy;
use Prophecy\Argument\Token;
/**
* Argument tokens shortcuts.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class Argument
{
/**
* Checks that argument is exact value or object.
*
* @param mixed $value
*
* @return Token\ExactValueToken
*/
public static function exact($value)
{
return new Token\ExactValueToken($value);
}
/**
* Checks that argument is of specific type or instance of specific class.
*
* @param string $type Type name (`integer`, `string`) or full class name
*
* @return Token\TypeToken
*/
public static function type($type)
{
return new Token\TypeToken($type);
}
/**
* Checks that argument object has specific state.
*
* @param string $methodName
* @param mixed $value
*
* @return Token\ObjectStateToken
*/
public static function which($methodName, $value)
{
return new Token\ObjectStateToken($methodName, $value);
}
/**
* Checks that argument matches provided callback.
*
* @param callable $callback
*
* @return Token\CallbackToken
*/
public static function that($callback)
{
return new Token\CallbackToken($callback);
}
/**
* Matches any single value.
*
* @return Token\AnyValueToken
*/
public static function any()
{
return new Token\AnyValueToken;
}
/**
* Matches all values to the rest of the signature.
*
* @return Token\AnyValuesToken
*/
public static function cetera()
{
return new Token\AnyValuesToken;
}
/**
* Checks that argument matches all tokens
*
* @param mixed ... a list of tokens
*
* @return Token\LogicalAndToken
*/
public static function allOf()
{
return new Token\LogicalAndToken(func_get_args());
}
/**
* Checks that argument array or countable object has exact number of elements.
*
* @param integer $value array elements count
*
* @return Token\ArrayCountToken
*/
public static function size($value)
{
return new Token\ArrayCountToken($value);
}
/**
* Checks that argument array contains (key, value) pair
*
* @param mixed $key exact value or token
* @param mixed $value exact value or token
*
* @return Token\ArrayEntryToken
*/
public static function withEntry($key, $value)
{
return new Token\ArrayEntryToken($key, $value);
}
/**
* Checks that arguments array entries all match value
*
* @param mixed $value
*
* @return Token\ArrayEveryEntryToken
*/
public static function withEveryEntry($value)
{
return new Token\ArrayEveryEntryToken($value);
}
/**
* Checks that argument array contains value
*
* @param mixed $value
*
* @return Token\ArrayEntryToken
*/
public static function containing($value)
{
return new Token\ArrayEntryToken(self::any(), $value);
}
/**
* Checks that argument array has key
*
* @param mixed $key exact value or token
*
* @return Token\ArrayEntryToken
*/
public static function withKey($key)
{
return new Token\ArrayEntryToken($key, self::any());
}
/**
* Checks that argument does not match the value|token.
*
* @param mixed $value either exact value or argument token
*
* @return Token\LogicalNotToken
*/
public static function not($value)
{
return new Token\LogicalNotToken($value);
}
/**
* @param string $value
*
* @return Token\StringContainsToken
*/
public static function containingString($value)
{
return new Token\StringContainsToken($value);
}
/**
* Checks that argument is identical value.
*
* @param mixed $value
*
* @return Token\IdenticalValueToken
*/
public static function is($value)
{
return new Token\IdenticalValueToken($value);
}
/**
* Check that argument is same value when rounding to the
* given precision.
*
* @param float $value
* @param float $precision
*
* @return Token\ApproximateValueToken
*/
public static function approximate($value, $precision = 0)
{
return new Token\ApproximateValueToken($value, $precision);
}
}

View File

@@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument;
/**
* Arguments wildcarding.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ArgumentsWildcard
{
/**
* @var Token\TokenInterface[]
*/
private $tokens = array();
private $string;
/**
* Initializes wildcard.
*
* @param array $arguments Array of argument tokens or values
*/
public function __construct(array $arguments)
{
foreach ($arguments as $argument) {
if (!$argument instanceof Token\TokenInterface) {
$argument = new Token\ExactValueToken($argument);
}
$this->tokens[] = $argument;
}
}
/**
* Calculates wildcard match score for provided arguments.
*
* @param array $arguments
*
* @return false|int False OR integer score (higher - better)
*/
public function scoreArguments(array $arguments)
{
if (0 == count($arguments) && 0 == count($this->tokens)) {
return 1;
}
$arguments = array_values($arguments);
$totalScore = 0;
foreach ($this->tokens as $i => $token) {
$argument = isset($arguments[$i]) ? $arguments[$i] : null;
if (1 >= $score = $token->scoreArgument($argument)) {
return false;
}
$totalScore += $score;
if (true === $token->isLast()) {
return $totalScore;
}
}
if (count($arguments) > count($this->tokens)) {
return false;
}
return $totalScore;
}
/**
* Returns string representation for wildcard.
*
* @return string
*/
public function __toString()
{
if (null === $this->string) {
$this->string = implode(', ', array_map(function ($token) {
return (string) $token;
}, $this->tokens));
}
return $this->string;
}
/**
* @return array
*/
public function getTokens()
{
return $this->tokens;
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
/**
* Any single value token.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class AnyValueToken implements TokenInterface
{
/**
* Always scores 3 for any argument.
*
* @param $argument
*
* @return int
*/
public function scoreArgument($argument)
{
return 3;
}
/**
* Returns false.
*
* @return bool
*/
public function isLast()
{
return false;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
return '*';
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
/**
* Any values token.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class AnyValuesToken implements TokenInterface
{
/**
* Always scores 2 for any argument.
*
* @param $argument
*
* @return int
*/
public function scoreArgument($argument)
{
return 2;
}
/**
* Returns true to stop wildcard from processing other tokens.
*
* @return bool
*/
public function isLast()
{
return true;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
return '* [, ...]';
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
/**
* Approximate value token
*
* @author Daniel Leech <daniel@dantleech.com>
*/
class ApproximateValueToken implements TokenInterface
{
private $value;
private $precision;
public function __construct($value, $precision = 0)
{
$this->value = $value;
$this->precision = $precision;
}
/**
* {@inheritdoc}
*/
public function scoreArgument($argument)
{
return round($argument, $this->precision) === round($this->value, $this->precision) ? 10 : false;
}
/**
* {@inheritdoc}
*/
public function isLast()
{
return false;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
return sprintf('≅%s', round($this->value, $this->precision));
}
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
/**
* Array elements count token.
*
* @author Boris Mikhaylov <kaguxmail@gmail.com>
*/
class ArrayCountToken implements TokenInterface
{
private $count;
/**
* @param integer $value
*/
public function __construct($value)
{
$this->count = $value;
}
/**
* Scores 6 when argument has preset number of elements.
*
* @param $argument
*
* @return bool|int
*/
public function scoreArgument($argument)
{
return $this->isCountable($argument) && $this->hasProperCount($argument) ? 6 : false;
}
/**
* Returns false.
*
* @return boolean
*/
public function isLast()
{
return false;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
return sprintf('count(%s)', $this->count);
}
/**
* Returns true if object is either array or instance of \Countable
*
* @param $argument
* @return bool
*/
private function isCountable($argument)
{
return (is_array($argument) || $argument instanceof \Countable);
}
/**
* Returns true if $argument has expected number of elements
*
* @param array|\Countable $argument
*
* @return bool
*/
private function hasProperCount($argument)
{
return $this->count === count($argument);
}
}

View File

@@ -0,0 +1,143 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
use Prophecy\Exception\InvalidArgumentException;
/**
* Array entry token.
*
* @author Boris Mikhaylov <kaguxmail@gmail.com>
*/
class ArrayEntryToken implements TokenInterface
{
/** @var \Prophecy\Argument\Token\TokenInterface */
private $key;
/** @var \Prophecy\Argument\Token\TokenInterface */
private $value;
/**
* @param mixed $key exact value or token
* @param mixed $value exact value or token
*/
public function __construct($key, $value)
{
$this->key = $this->wrapIntoExactValueToken($key);
$this->value = $this->wrapIntoExactValueToken($value);
}
/**
* Scores half of combined scores from key and value tokens for same entry. Capped at 8.
* If argument implements \ArrayAccess without \Traversable, then key token is restricted to ExactValueToken.
*
* @param array|\ArrayAccess|\Traversable $argument
*
* @throws \Prophecy\Exception\InvalidArgumentException
* @return bool|int
*/
public function scoreArgument($argument)
{
if ($argument instanceof \Traversable) {
$argument = iterator_to_array($argument);
}
if ($argument instanceof \ArrayAccess) {
$argument = $this->convertArrayAccessToEntry($argument);
}
if (!is_array($argument) || empty($argument)) {
return false;
}
$keyScores = array_map(array($this->key,'scoreArgument'), array_keys($argument));
$valueScores = array_map(array($this->value,'scoreArgument'), $argument);
$scoreEntry = function ($value, $key) {
return $value && $key ? min(8, ($key + $value) / 2) : false;
};
return max(array_map($scoreEntry, $valueScores, $keyScores));
}
/**
* Returns false.
*
* @return boolean
*/
public function isLast()
{
return false;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
return sprintf('[..., %s => %s, ...]', $this->key, $this->value);
}
/**
* Returns key
*
* @return TokenInterface
*/
public function getKey()
{
return $this->key;
}
/**
* Returns value
*
* @return TokenInterface
*/
public function getValue()
{
return $this->value;
}
/**
* Wraps non token $value into ExactValueToken
*
* @param $value
* @return TokenInterface
*/
private function wrapIntoExactValueToken($value)
{
return $value instanceof TokenInterface ? $value : new ExactValueToken($value);
}
/**
* Converts instance of \ArrayAccess to key => value array entry
*
* @param \ArrayAccess $object
*
* @return array|null
* @throws \Prophecy\Exception\InvalidArgumentException
*/
private function convertArrayAccessToEntry(\ArrayAccess $object)
{
if (!$this->key instanceof ExactValueToken) {
throw new InvalidArgumentException(sprintf(
'You can only use exact value tokens to match key of ArrayAccess object'.PHP_EOL.
'But you used `%s`.',
$this->key
));
}
$key = $this->key->getValue();
return $object->offsetExists($key) ? array($key => $object[$key]) : array();
}
}

View File

@@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
/**
* Array every entry token.
*
* @author Adrien Brault <adrien.brault@gmail.com>
*/
class ArrayEveryEntryToken implements TokenInterface
{
/**
* @var TokenInterface
*/
private $value;
/**
* @param mixed $value exact value or token
*/
public function __construct($value)
{
if (!$value instanceof TokenInterface) {
$value = new ExactValueToken($value);
}
$this->value = $value;
}
/**
* {@inheritdoc}
*/
public function scoreArgument($argument)
{
if (!$argument instanceof \Traversable && !is_array($argument)) {
return false;
}
$scores = array();
foreach ($argument as $key => $argumentEntry) {
$scores[] = $this->value->scoreArgument($argumentEntry);
}
if (empty($scores) || in_array(false, $scores, true)) {
return false;
}
return array_sum($scores) / count($scores);
}
/**
* {@inheritdoc}
*/
public function isLast()
{
return false;
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return sprintf('[%s, ..., %s]', $this->value, $this->value);
}
/**
* @return TokenInterface
*/
public function getValue()
{
return $this->value;
}
}

View File

@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
use Prophecy\Exception\InvalidArgumentException;
/**
* Callback-verified token.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class CallbackToken implements TokenInterface
{
private $callback;
/**
* Initializes token.
*
* @param callable $callback
*
* @throws \Prophecy\Exception\InvalidArgumentException
*/
public function __construct($callback)
{
if (!is_callable($callback)) {
throw new InvalidArgumentException(sprintf(
'Callable expected as an argument to CallbackToken, but got %s.',
gettype($callback)
));
}
$this->callback = $callback;
}
/**
* Scores 7 if callback returns true, false otherwise.
*
* @param $argument
*
* @return bool|int
*/
public function scoreArgument($argument)
{
return call_user_func($this->callback, $argument) ? 7 : false;
}
/**
* Returns false.
*
* @return bool
*/
public function isLast()
{
return false;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
return 'callback()';
}
}

View File

@@ -0,0 +1,116 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
use SebastianBergmann\Comparator\ComparisonFailure;
use Prophecy\Comparator\Factory as ComparatorFactory;
use Prophecy\Util\StringUtil;
/**
* Exact value token.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ExactValueToken implements TokenInterface
{
private $value;
private $string;
private $util;
private $comparatorFactory;
/**
* Initializes token.
*
* @param mixed $value
* @param StringUtil $util
* @param ComparatorFactory $comparatorFactory
*/
public function __construct($value, StringUtil $util = null, ComparatorFactory $comparatorFactory = null)
{
$this->value = $value;
$this->util = $util ?: new StringUtil();
$this->comparatorFactory = $comparatorFactory ?: ComparatorFactory::getInstance();
}
/**
* Scores 10 if argument matches preset value.
*
* @param $argument
*
* @return bool|int
*/
public function scoreArgument($argument)
{
if (is_object($argument) && is_object($this->value)) {
$comparator = $this->comparatorFactory->getComparatorFor(
$argument, $this->value
);
try {
$comparator->assertEquals($argument, $this->value);
return 10;
} catch (ComparisonFailure $failure) {}
}
// If either one is an object it should be castable to a string
if (is_object($argument) xor is_object($this->value)) {
if (is_object($argument) && !method_exists($argument, '__toString')) {
return false;
}
if (is_object($this->value) && !method_exists($this->value, '__toString')) {
return false;
}
} elseif (is_numeric($argument) && is_numeric($this->value)) {
// noop
} elseif (gettype($argument) !== gettype($this->value)) {
return false;
}
return $argument == $this->value ? 10 : false;
}
/**
* Returns preset value against which token checks arguments.
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* Returns false.
*
* @return bool
*/
public function isLast()
{
return false;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
if (null === $this->string) {
$this->string = sprintf('exact(%s)', $this->util->stringify($this->value));
}
return $this->string;
}
}

View File

@@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
use Prophecy\Util\StringUtil;
/**
* Identical value token.
*
* @author Florian Voutzinos <florian@voutzinos.com>
*/
class IdenticalValueToken implements TokenInterface
{
private $value;
private $string;
private $util;
/**
* Initializes token.
*
* @param mixed $value
* @param StringUtil $util
*/
public function __construct($value, StringUtil $util = null)
{
$this->value = $value;
$this->util = $util ?: new StringUtil();
}
/**
* Scores 11 if argument matches preset value.
*
* @param $argument
*
* @return bool|int
*/
public function scoreArgument($argument)
{
return $argument === $this->value ? 11 : false;
}
/**
* Returns false.
*
* @return bool
*/
public function isLast()
{
return false;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
if (null === $this->string) {
$this->string = sprintf('identical(%s)', $this->util->stringify($this->value));
}
return $this->string;
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
/**
* Logical AND token.
*
* @author Boris Mikhaylov <kaguxmail@gmail.com>
*/
class LogicalAndToken implements TokenInterface
{
private $tokens = array();
/**
* @param array $arguments exact values or tokens
*/
public function __construct(array $arguments)
{
foreach ($arguments as $argument) {
if (!$argument instanceof TokenInterface) {
$argument = new ExactValueToken($argument);
}
$this->tokens[] = $argument;
}
}
/**
* Scores maximum score from scores returned by tokens for this argument if all of them score.
*
* @param $argument
*
* @return bool|int
*/
public function scoreArgument($argument)
{
if (0 === count($this->tokens)) {
return false;
}
$maxScore = 0;
foreach ($this->tokens as $token) {
$score = $token->scoreArgument($argument);
if (false === $score) {
return false;
}
$maxScore = max($score, $maxScore);
}
return $maxScore;
}
/**
* Returns false.
*
* @return boolean
*/
public function isLast()
{
return false;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
return sprintf('bool(%s)', implode(' AND ', $this->tokens));
}
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
/**
* Logical NOT token.
*
* @author Boris Mikhaylov <kaguxmail@gmail.com>
*/
class LogicalNotToken implements TokenInterface
{
/** @var \Prophecy\Argument\Token\TokenInterface */
private $token;
/**
* @param mixed $value exact value or token
*/
public function __construct($value)
{
$this->token = $value instanceof TokenInterface? $value : new ExactValueToken($value);
}
/**
* Scores 4 when preset token does not match the argument.
*
* @param $argument
*
* @return bool|int
*/
public function scoreArgument($argument)
{
return false === $this->token->scoreArgument($argument) ? 4 : false;
}
/**
* Returns true if preset token is last.
*
* @return bool|int
*/
public function isLast()
{
return $this->token->isLast();
}
/**
* Returns originating token.
*
* @return TokenInterface
*/
public function getOriginatingToken()
{
return $this->token;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
return sprintf('not(%s)', $this->token);
}
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
use SebastianBergmann\Comparator\ComparisonFailure;
use Prophecy\Comparator\Factory as ComparatorFactory;
use Prophecy\Util\StringUtil;
/**
* Object state-checker token.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ObjectStateToken implements TokenInterface
{
private $name;
private $value;
private $util;
private $comparatorFactory;
/**
* Initializes token.
*
* @param string $methodName
* @param mixed $value Expected return value
* @param null|StringUtil $util
* @param ComparatorFactory $comparatorFactory
*/
public function __construct(
$methodName,
$value,
StringUtil $util = null,
ComparatorFactory $comparatorFactory = null
) {
$this->name = $methodName;
$this->value = $value;
$this->util = $util ?: new StringUtil;
$this->comparatorFactory = $comparatorFactory ?: ComparatorFactory::getInstance();
}
/**
* Scores 8 if argument is an object, which method returns expected value.
*
* @param mixed $argument
*
* @return bool|int
*/
public function scoreArgument($argument)
{
if (is_object($argument) && method_exists($argument, $this->name)) {
$actual = call_user_func(array($argument, $this->name));
$comparator = $this->comparatorFactory->getComparatorFor(
$this->value, $actual
);
try {
$comparator->assertEquals($this->value, $actual);
return 8;
} catch (ComparisonFailure $failure) {
return false;
}
}
if (is_object($argument) && property_exists($argument, $this->name)) {
return $argument->{$this->name} === $this->value ? 8 : false;
}
return false;
}
/**
* Returns false.
*
* @return bool
*/
public function isLast()
{
return false;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
return sprintf('state(%s(), %s)',
$this->name,
$this->util->stringify($this->value)
);
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
/**
* String contains token.
*
* @author Peter Mitchell <pete@peterjmit.com>
*/
class StringContainsToken implements TokenInterface
{
private $value;
/**
* Initializes token.
*
* @param string $value
*/
public function __construct($value)
{
$this->value = $value;
}
public function scoreArgument($argument)
{
return is_string($argument) && strpos($argument, $this->value) !== false ? 6 : false;
}
/**
* Returns preset value against which token checks arguments.
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* Returns false.
*
* @return bool
*/
public function isLast()
{
return false;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
return sprintf('contains("%s")', $this->value);
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
/**
* Argument token interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface TokenInterface
{
/**
* Calculates token match score for provided argument.
*
* @param $argument
*
* @return bool|int
*/
public function scoreArgument($argument);
/**
* Returns true if this token prevents check of other tokens (is last one).
*
* @return bool|int
*/
public function isLast();
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString();
}

View File

@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Argument\Token;
use Prophecy\Exception\InvalidArgumentException;
/**
* Value type token.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class TypeToken implements TokenInterface
{
private $type;
/**
* @param string $type
*/
public function __construct($type)
{
$checker = "is_{$type}";
if (!function_exists($checker) && !interface_exists($type) && !class_exists($type)) {
throw new InvalidArgumentException(sprintf(
'Type or class name expected as an argument to TypeToken, but got %s.', $type
));
}
$this->type = $type;
}
/**
* Scores 5 if argument has the same type this token was constructed with.
*
* @param $argument
*
* @return bool|int
*/
public function scoreArgument($argument)
{
$checker = "is_{$this->type}";
if (function_exists($checker)) {
return call_user_func($checker, $argument) ? 5 : false;
}
return $argument instanceof $this->type ? 5 : false;
}
/**
* Returns false.
*
* @return bool
*/
public function isLast()
{
return false;
}
/**
* Returns string representation for token.
*
* @return string
*/
public function __toString()
{
return sprintf('type(%s)', $this->type);
}
}

View File

@@ -0,0 +1,162 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Call;
use Exception;
use Prophecy\Argument\ArgumentsWildcard;
/**
* Call object.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class Call
{
private $methodName;
private $arguments;
private $returnValue;
private $exception;
private $file;
private $line;
private $scores;
/**
* Initializes call.
*
* @param string $methodName
* @param array $arguments
* @param mixed $returnValue
* @param Exception $exception
* @param null|string $file
* @param null|int $line
*/
public function __construct($methodName, array $arguments, $returnValue,
Exception $exception = null, $file, $line)
{
$this->methodName = $methodName;
$this->arguments = $arguments;
$this->returnValue = $returnValue;
$this->exception = $exception;
$this->scores = new \SplObjectStorage();
if ($file) {
$this->file = $file;
$this->line = intval($line);
}
}
/**
* Returns called method name.
*
* @return string
*/
public function getMethodName()
{
return $this->methodName;
}
/**
* Returns called method arguments.
*
* @return array
*/
public function getArguments()
{
return $this->arguments;
}
/**
* Returns called method return value.
*
* @return null|mixed
*/
public function getReturnValue()
{
return $this->returnValue;
}
/**
* Returns exception that call thrown.
*
* @return null|Exception
*/
public function getException()
{
return $this->exception;
}
/**
* Returns callee filename.
*
* @return string
*/
public function getFile()
{
return $this->file;
}
/**
* Returns callee line number.
*
* @return int
*/
public function getLine()
{
return $this->line;
}
/**
* Returns short notation for callee place.
*
* @return string
*/
public function getCallPlace()
{
if (null === $this->file) {
return 'unknown';
}
return sprintf('%s:%d', $this->file, $this->line);
}
/**
* Adds the wildcard match score for the provided wildcard.
*
* @param ArgumentsWildcard $wildcard
* @param false|int $score
*
* @return $this
*/
public function addScore(ArgumentsWildcard $wildcard, $score)
{
$this->scores[$wildcard] = $score;
return $this;
}
/**
* Returns wildcard match score for the provided wildcard. The score is
* calculated if not already done.
*
* @param ArgumentsWildcard $wildcard
*
* @return false|int False OR integer score (higher - better)
*/
public function getScore(ArgumentsWildcard $wildcard)
{
if (isset($this->scores[$wildcard])) {
return $this->scores[$wildcard];
}
return $this->scores[$wildcard] = $wildcard->scoreArguments($this->getArguments());
}
}

View File

@@ -0,0 +1,229 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Call;
use Prophecy\Exception\Prophecy\MethodProphecyException;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Argument\ArgumentsWildcard;
use Prophecy\Util\StringUtil;
use Prophecy\Exception\Call\UnexpectedCallException;
/**
* Calls receiver & manager.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class CallCenter
{
private $util;
/**
* @var Call[]
*/
private $recordedCalls = array();
/**
* Initializes call center.
*
* @param StringUtil $util
*/
public function __construct(StringUtil $util = null)
{
$this->util = $util ?: new StringUtil;
}
/**
* Makes and records specific method call for object prophecy.
*
* @param ObjectProphecy $prophecy
* @param string $methodName
* @param array $arguments
*
* @return mixed Returns null if no promise for prophecy found or promise return value.
*
* @throws \Prophecy\Exception\Call\UnexpectedCallException If no appropriate method prophecy found
*/
public function makeCall(ObjectProphecy $prophecy, $methodName, array $arguments)
{
// For efficiency exclude 'args' from the generated backtrace
if (PHP_VERSION_ID >= 50400) {
// Limit backtrace to last 3 calls as we don't use the rest
// Limit argument was introduced in PHP 5.4.0
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
} elseif (defined('DEBUG_BACKTRACE_IGNORE_ARGS')) {
// DEBUG_BACKTRACE_IGNORE_ARGS was introduced in PHP 5.3.6
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
} else {
$backtrace = debug_backtrace();
}
$file = $line = null;
if (isset($backtrace[2]) && isset($backtrace[2]['file'])) {
$file = $backtrace[2]['file'];
$line = $backtrace[2]['line'];
}
// If no method prophecies defined, then it's a dummy, so we'll just return null
if ('__destruct' === $methodName || 0 == count($prophecy->getMethodProphecies())) {
$this->recordedCalls[] = new Call($methodName, $arguments, null, null, $file, $line);
return null;
}
// There are method prophecies, so it's a fake/stub. Searching prophecy for this call
$matches = array();
foreach ($prophecy->getMethodProphecies($methodName) as $methodProphecy) {
if (0 < $score = $methodProphecy->getArgumentsWildcard()->scoreArguments($arguments)) {
$matches[] = array($score, $methodProphecy);
}
}
// If fake/stub doesn't have method prophecy for this call - throw exception
if (!count($matches)) {
throw $this->createUnexpectedCallException($prophecy, $methodName, $arguments);
}
// Sort matches by their score value
@usort($matches, function ($match1, $match2) { return $match2[0] - $match1[0]; });
$score = $matches[0][0];
// If Highest rated method prophecy has a promise - execute it or return null instead
$methodProphecy = $matches[0][1];
$returnValue = null;
$exception = null;
if ($promise = $methodProphecy->getPromise()) {
try {
$returnValue = $promise->execute($arguments, $prophecy, $methodProphecy);
} catch (\Exception $e) {
$exception = $e;
}
}
if ($methodProphecy->hasReturnVoid() && $returnValue !== null) {
throw new MethodProphecyException(
"The method \"$methodName\" has a void return type, but the promise returned a value",
$methodProphecy
);
}
$this->recordedCalls[] = $call = new Call(
$methodName, $arguments, $returnValue, $exception, $file, $line
);
$call->addScore($methodProphecy->getArgumentsWildcard(), $score);
if (null !== $exception) {
throw $exception;
}
return $returnValue;
}
/**
* Searches for calls by method name & arguments wildcard.
*
* @param string $methodName
* @param ArgumentsWildcard $wildcard
*
* @return Call[]
*/
public function findCalls($methodName, ArgumentsWildcard $wildcard)
{
return array_values(
array_filter($this->recordedCalls, function (Call $call) use ($methodName, $wildcard) {
return $methodName === $call->getMethodName()
&& 0 < $call->getScore($wildcard)
;
})
);
}
private function createUnexpectedCallException(ObjectProphecy $prophecy, $methodName,
array $arguments)
{
$classname = get_class($prophecy->reveal());
$indentationLength = 8; // looks good
$argstring = implode(
",\n",
$this->indentArguments(
array_map(array($this->util, 'stringify'), $arguments),
$indentationLength
)
);
$expected = array();
foreach (call_user_func_array('array_merge', $prophecy->getMethodProphecies()) as $methodProphecy) {
$expected[] = sprintf(
" - %s(\n" .
"%s\n" .
" )",
$methodProphecy->getMethodName(),
implode(
",\n",
$this->indentArguments(
array_map('strval', $methodProphecy->getArgumentsWildcard()->getTokens()),
$indentationLength
)
)
);
}
return new UnexpectedCallException(
sprintf(
"Unexpected method call on %s:\n".
" - %s(\n".
"%s\n".
" )\n".
"expected calls were:\n".
"%s",
$classname, $methodName, $argstring, implode("\n", $expected)
),
$prophecy, $methodName, $arguments
);
}
private function formatExceptionMessage(MethodProphecy $methodProphecy)
{
return sprintf(
" - %s(\n".
"%s\n".
" )",
$methodProphecy->getMethodName(),
implode(
",\n",
$this->indentArguments(
array_map(
function ($token) {
return (string) $token;
},
$methodProphecy->getArgumentsWildcard()->getTokens()
),
$indentationLength
)
)
);
}
private function indentArguments(array $arguments, $indentationLength)
{
return preg_replace_callback(
'/^/m',
function () use ($indentationLength) {
return str_repeat(' ', $indentationLength);
},
$arguments
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Comparator;
use SebastianBergmann\Comparator\Comparator;
use SebastianBergmann\Comparator\ComparisonFailure;
/**
* Closure comparator.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class ClosureComparator extends Comparator
{
public function accepts($expected, $actual)
{
return is_object($expected) && $expected instanceof \Closure
&& is_object($actual) && $actual instanceof \Closure;
}
public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false)
{
throw new ComparisonFailure(
$expected,
$actual,
// we don't need a diff
'',
'',
false,
'all closures are born different'
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Comparator;
use SebastianBergmann\Comparator\Factory as BaseFactory;
/**
* Prophecy comparator factory.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
final class Factory extends BaseFactory
{
/**
* @var Factory
*/
private static $instance;
public function __construct()
{
parent::__construct();
$this->register(new ClosureComparator());
$this->register(new ProphecyComparator());
}
/**
* @return Factory
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new Factory;
}
return self::$instance;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Comparator;
use Prophecy\Prophecy\ProphecyInterface;
use SebastianBergmann\Comparator\ObjectComparator;
class ProphecyComparator extends ObjectComparator
{
public function accepts($expected, $actual)
{
return is_object($expected) && is_object($actual) && $actual instanceof ProphecyInterface;
}
public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false, array &$processed = array())
{
parent::assertEquals($expected, $actual->reveal(), $delta, $canonicalize, $ignoreCase, $processed);
}
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler;
use ReflectionClass;
/**
* Cached class doubler.
* Prevents mirroring/creation of the same structure twice.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class CachedDoubler extends Doubler
{
private $classes = array();
/**
* {@inheritdoc}
*/
public function registerClassPatch(ClassPatch\ClassPatchInterface $patch)
{
$this->classes[] = array();
parent::registerClassPatch($patch);
}
/**
* {@inheritdoc}
*/
protected function createDoubleClass(ReflectionClass $class = null, array $interfaces)
{
$classId = $this->generateClassId($class, $interfaces);
if (isset($this->classes[$classId])) {
return $this->classes[$classId];
}
return $this->classes[$classId] = parent::createDoubleClass($class, $interfaces);
}
/**
* @param ReflectionClass $class
* @param ReflectionClass[] $interfaces
*
* @return string
*/
private function generateClassId(ReflectionClass $class = null, array $interfaces)
{
$parts = array();
if (null !== $class) {
$parts[] = $class->getName();
}
foreach ($interfaces as $interface) {
$parts[] = $interface->getName();
}
sort($parts);
return md5(implode('', $parts));
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\ClassPatch;
use Prophecy\Doubler\Generator\Node\ClassNode;
/**
* Class patch interface.
* Class patches extend doubles functionality or help
* Prophecy to avoid some internal PHP bugs.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ClassPatchInterface
{
/**
* Checks if patch supports specific class node.
*
* @param ClassNode $node
*
* @return bool
*/
public function supports(ClassNode $node);
/**
* Applies patch to the specific class node.
*
* @param ClassNode $node
* @return void
*/
public function apply(ClassNode $node);
/**
* Returns patch priority, which determines when patch will be applied.
*
* @return int Priority number (higher - earlier)
*/
public function getPriority();
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\ClassPatch;
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Doubler\Generator\Node\MethodNode;
/**
* Disable constructor.
* Makes all constructor arguments optional.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class DisableConstructorPatch implements ClassPatchInterface
{
/**
* Checks if class has `__construct` method.
*
* @param ClassNode $node
*
* @return bool
*/
public function supports(ClassNode $node)
{
return true;
}
/**
* Makes all class constructor arguments optional.
*
* @param ClassNode $node
*/
public function apply(ClassNode $node)
{
if (!$node->hasMethod('__construct')) {
$node->addMethod(new MethodNode('__construct', ''));
return;
}
$constructor = $node->getMethod('__construct');
foreach ($constructor->getArguments() as $argument) {
$argument->setDefault(null);
}
$constructor->setCode(<<<PHP
if (0 < func_num_args()) {
call_user_func_array(array('parent', '__construct'), func_get_args());
}
PHP
);
}
/**
* Returns patch priority, which determines when patch will be applied.
*
* @return int Priority number (higher - earlier)
*/
public function getPriority()
{
return 100;
}
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\ClassPatch;
use Prophecy\Doubler\Generator\Node\ClassNode;
/**
* Exception patch for HHVM to remove the stubs from special methods
*
* @author Christophe Coevoet <stof@notk.org>
*/
class HhvmExceptionPatch implements ClassPatchInterface
{
/**
* Supports exceptions on HHVM.
*
* @param ClassNode $node
*
* @return bool
*/
public function supports(ClassNode $node)
{
if (!defined('HHVM_VERSION')) {
return false;
}
return 'Exception' === $node->getParentClass() || is_subclass_of($node->getParentClass(), 'Exception');
}
/**
* Removes special exception static methods from the doubled methods.
*
* @param ClassNode $node
*
* @return void
*/
public function apply(ClassNode $node)
{
if ($node->hasMethod('setTraceOptions')) {
$node->getMethod('setTraceOptions')->useParentCode();
}
if ($node->hasMethod('getTraceOptions')) {
$node->getMethod('getTraceOptions')->useParentCode();
}
}
/**
* {@inheritdoc}
*/
public function getPriority()
{
return -50;
}
}

View File

@@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\ClassPatch;
use Prophecy\Doubler\Generator\Node\ClassNode;
/**
* Remove method functionality from the double which will clash with php keywords.
*
* @author Milan Magudia <milan@magudia.com>
*/
class KeywordPatch implements ClassPatchInterface
{
/**
* Support any class
*
* @param ClassNode $node
*
* @return boolean
*/
public function supports(ClassNode $node)
{
return true;
}
/**
* Remove methods that clash with php keywords
*
* @param ClassNode $node
*/
public function apply(ClassNode $node)
{
$methodNames = array_keys($node->getMethods());
$methodsToRemove = array_intersect($methodNames, $this->getKeywords());
foreach ($methodsToRemove as $methodName) {
$node->removeMethod($methodName);
}
}
/**
* Returns patch priority, which determines when patch will be applied.
*
* @return int Priority number (higher - earlier)
*/
public function getPriority()
{
return 49;
}
/**
* Returns array of php keywords.
*
* @return array
*/
private function getKeywords()
{
if (\PHP_VERSION_ID >= 70000) {
return array('__halt_compiler');
}
return array(
'__halt_compiler',
'abstract',
'and',
'array',
'as',
'break',
'callable',
'case',
'catch',
'class',
'clone',
'const',
'continue',
'declare',
'default',
'die',
'do',
'echo',
'else',
'elseif',
'empty',
'enddeclare',
'endfor',
'endforeach',
'endif',
'endswitch',
'endwhile',
'eval',
'exit',
'extends',
'final',
'finally',
'for',
'foreach',
'function',
'global',
'goto',
'if',
'implements',
'include',
'include_once',
'instanceof',
'insteadof',
'interface',
'isset',
'list',
'namespace',
'new',
'or',
'print',
'private',
'protected',
'public',
'require',
'require_once',
'return',
'static',
'switch',
'throw',
'trait',
'try',
'unset',
'use',
'var',
'while',
'xor',
'yield',
);
}
}

View File

@@ -0,0 +1,94 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\ClassPatch;
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Doubler\Generator\Node\MethodNode;
use Prophecy\PhpDocumentor\ClassAndInterfaceTagRetriever;
use Prophecy\PhpDocumentor\MethodTagRetrieverInterface;
/**
* Discover Magical API using "@method" PHPDoc format.
*
* @author Thomas Tourlourat <thomas@tourlourat.com>
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Théo FIDRY <theo.fidry@gmail.com>
*/
class MagicCallPatch implements ClassPatchInterface
{
private $tagRetriever;
public function __construct(MethodTagRetrieverInterface $tagRetriever = null)
{
$this->tagRetriever = null === $tagRetriever ? new ClassAndInterfaceTagRetriever() : $tagRetriever;
}
/**
* Support any class
*
* @param ClassNode $node
*
* @return boolean
*/
public function supports(ClassNode $node)
{
return true;
}
/**
* Discover Magical API
*
* @param ClassNode $node
*/
public function apply(ClassNode $node)
{
$types = array_filter($node->getInterfaces(), function ($interface) {
return 0 !== strpos($interface, 'Prophecy\\');
});
$types[] = $node->getParentClass();
foreach ($types as $type) {
$reflectionClass = new \ReflectionClass($type);
while ($reflectionClass) {
$tagList = $this->tagRetriever->getTagList($reflectionClass);
foreach ($tagList as $tag) {
$methodName = $tag->getMethodName();
if (empty($methodName)) {
continue;
}
if (!$reflectionClass->hasMethod($methodName)) {
$methodNode = new MethodNode($methodName);
$methodNode->setStatic($tag->isStatic());
$node->addMethod($methodNode);
}
}
$reflectionClass = $reflectionClass->getParentClass();
}
}
}
/**
* Returns patch priority, which determines when patch will be applied.
*
* @return integer Priority number (higher - earlier)
*/
public function getPriority()
{
return 50;
}
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\ClassPatch;
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Doubler\Generator\Node\MethodNode;
use Prophecy\Doubler\Generator\Node\ArgumentNode;
/**
* Add Prophecy functionality to the double.
* This is a core class patch for Prophecy.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ProphecySubjectPatch implements ClassPatchInterface
{
/**
* Always returns true.
*
* @param ClassNode $node
*
* @return bool
*/
public function supports(ClassNode $node)
{
return true;
}
/**
* Apply Prophecy functionality to class node.
*
* @param ClassNode $node
*/
public function apply(ClassNode $node)
{
$node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface');
$node->addProperty('objectProphecy', 'private');
foreach ($node->getMethods() as $name => $method) {
if ('__construct' === strtolower($name)) {
continue;
}
if ($method->getReturnType() === 'void') {
$method->setCode(
'$this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());'
);
} else {
$method->setCode(
'return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());'
);
}
}
$prophecySetter = new MethodNode('setProphecy');
$prophecyArgument = new ArgumentNode('prophecy');
$prophecyArgument->setTypeHint('Prophecy\Prophecy\ProphecyInterface');
$prophecySetter->addArgument($prophecyArgument);
$prophecySetter->setCode('$this->objectProphecy = $prophecy;');
$prophecyGetter = new MethodNode('getProphecy');
$prophecyGetter->setCode('return $this->objectProphecy;');
if ($node->hasMethod('__call')) {
$__call = $node->getMethod('__call');
} else {
$__call = new MethodNode('__call');
$__call->addArgument(new ArgumentNode('name'));
$__call->addArgument(new ArgumentNode('arguments'));
$node->addMethod($__call, true);
}
$__call->setCode(<<<PHP
throw new \Prophecy\Exception\Doubler\MethodNotFoundException(
sprintf('Method `%s::%s()` not found.', get_class(\$this), func_get_arg(0)),
\$this->getProphecy(), func_get_arg(0)
);
PHP
);
$node->addMethod($prophecySetter, true);
$node->addMethod($prophecyGetter, true);
}
/**
* Returns patch priority, which determines when patch will be applied.
*
* @return int Priority number (higher - earlier)
*/
public function getPriority()
{
return 0;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\ClassPatch;
use Prophecy\Doubler\Generator\Node\ClassNode;
/**
* ReflectionClass::newInstance patch.
* Makes first argument of newInstance optional, since it works but signature is misleading
*
* @author Florian Klein <florian.klein@free.fr>
*/
class ReflectionClassNewInstancePatch implements ClassPatchInterface
{
/**
* Supports ReflectionClass
*
* @param ClassNode $node
*
* @return bool
*/
public function supports(ClassNode $node)
{
return 'ReflectionClass' === $node->getParentClass();
}
/**
* Updates newInstance's first argument to make it optional
*
* @param ClassNode $node
*/
public function apply(ClassNode $node)
{
foreach ($node->getMethod('newInstance')->getArguments() as $argument) {
$argument->setDefault(null);
}
}
/**
* Returns patch priority, which determines when patch will be applied.
*
* @return int Priority number (higher = earlier)
*/
public function getPriority()
{
return 50;
}
}

View File

@@ -0,0 +1,123 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\ClassPatch;
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Doubler\Generator\Node\MethodNode;
/**
* SplFileInfo patch.
* Makes SplFileInfo and derivative classes usable with Prophecy.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class SplFileInfoPatch implements ClassPatchInterface
{
/**
* Supports everything that extends SplFileInfo.
*
* @param ClassNode $node
*
* @return bool
*/
public function supports(ClassNode $node)
{
if (null === $node->getParentClass()) {
return false;
}
return 'SplFileInfo' === $node->getParentClass()
|| is_subclass_of($node->getParentClass(), 'SplFileInfo')
;
}
/**
* Updated constructor code to call parent one with dummy file argument.
*
* @param ClassNode $node
*/
public function apply(ClassNode $node)
{
if ($node->hasMethod('__construct')) {
$constructor = $node->getMethod('__construct');
} else {
$constructor = new MethodNode('__construct');
$node->addMethod($constructor);
}
if ($this->nodeIsDirectoryIterator($node)) {
$constructor->setCode('return parent::__construct("' . __DIR__ . '");');
return;
}
if ($this->nodeIsSplFileObject($node)) {
$filePath = str_replace('\\','\\\\',__FILE__);
$constructor->setCode('return parent::__construct("' . $filePath .'");');
return;
}
if ($this->nodeIsSymfonySplFileInfo($node)) {
$filePath = str_replace('\\','\\\\',__FILE__);
$constructor->setCode('return parent::__construct("' . $filePath .'", "", "");');
return;
}
$constructor->useParentCode();
}
/**
* Returns patch priority, which determines when patch will be applied.
*
* @return int Priority number (higher - earlier)
*/
public function getPriority()
{
return 50;
}
/**
* @param ClassNode $node
* @return boolean
*/
private function nodeIsDirectoryIterator(ClassNode $node)
{
$parent = $node->getParentClass();
return 'DirectoryIterator' === $parent
|| is_subclass_of($parent, 'DirectoryIterator');
}
/**
* @param ClassNode $node
* @return boolean
*/
private function nodeIsSplFileObject(ClassNode $node)
{
$parent = $node->getParentClass();
return 'SplFileObject' === $parent
|| is_subclass_of($parent, 'SplFileObject');
}
/**
* @param ClassNode $node
* @return boolean
*/
private function nodeIsSymfonySplFileInfo(ClassNode $node)
{
$parent = $node->getParentClass();
return 'Symfony\\Component\\Finder\\SplFileInfo' === $parent;
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Prophecy\Doubler\ClassPatch;
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Exception\Doubler\ClassCreatorException;
class ThrowablePatch implements ClassPatchInterface
{
/**
* Checks if patch supports specific class node.
*
* @param ClassNode $node
* @return bool
*/
public function supports(ClassNode $node)
{
return $this->implementsAThrowableInterface($node) && $this->doesNotExtendAThrowableClass($node);
}
/**
* @param ClassNode $node
* @return bool
*/
private function implementsAThrowableInterface(ClassNode $node)
{
foreach ($node->getInterfaces() as $type) {
if (is_a($type, 'Throwable', true)) {
return true;
}
}
return false;
}
/**
* @param ClassNode $node
* @return bool
*/
private function doesNotExtendAThrowableClass(ClassNode $node)
{
return !is_a($node->getParentClass(), 'Throwable', true);
}
/**
* Applies patch to the specific class node.
*
* @param ClassNode $node
*
* @return void
*/
public function apply(ClassNode $node)
{
$this->checkItCanBeDoubled($node);
$this->setParentClassToException($node);
}
private function checkItCanBeDoubled(ClassNode $node)
{
$className = $node->getParentClass();
if ($className !== 'stdClass') {
throw new ClassCreatorException(
sprintf(
'Cannot double concrete class %s as well as implement Traversable',
$className
),
$node
);
}
}
private function setParentClassToException(ClassNode $node)
{
$node->setParentClass('Exception');
$node->removeMethod('getMessage');
$node->removeMethod('getCode');
$node->removeMethod('getFile');
$node->removeMethod('getLine');
$node->removeMethod('getTrace');
$node->removeMethod('getPrevious');
$node->removeMethod('getNext');
$node->removeMethod('getTraceAsString');
}
/**
* Returns patch priority, which determines when patch will be applied.
*
* @return int Priority number (higher - earlier)
*/
public function getPriority()
{
return 100;
}
}

View File

@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\ClassPatch;
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Doubler\Generator\Node\MethodNode;
/**
* Traversable interface patch.
* Forces classes that implement interfaces, that extend Traversable to also implement Iterator.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class TraversablePatch implements ClassPatchInterface
{
/**
* Supports nodetree, that implement Traversable, but not Iterator or IteratorAggregate.
*
* @param ClassNode $node
*
* @return bool
*/
public function supports(ClassNode $node)
{
if (in_array('Iterator', $node->getInterfaces())) {
return false;
}
if (in_array('IteratorAggregate', $node->getInterfaces())) {
return false;
}
foreach ($node->getInterfaces() as $interface) {
if ('Traversable' !== $interface && !is_subclass_of($interface, 'Traversable')) {
continue;
}
if ('Iterator' === $interface || is_subclass_of($interface, 'Iterator')) {
continue;
}
if ('IteratorAggregate' === $interface || is_subclass_of($interface, 'IteratorAggregate')) {
continue;
}
return true;
}
return false;
}
/**
* Forces class to implement Iterator interface.
*
* @param ClassNode $node
*/
public function apply(ClassNode $node)
{
$node->addInterface('Iterator');
$node->addMethod(new MethodNode('current'));
$node->addMethod(new MethodNode('key'));
$node->addMethod(new MethodNode('next'));
$node->addMethod(new MethodNode('rewind'));
$node->addMethod(new MethodNode('valid'));
}
/**
* Returns patch priority, which determines when patch will be applied.
*
* @return int Priority number (higher - earlier)
*/
public function getPriority()
{
return 100;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler;
/**
* Core double interface.
* All doubled classes will implement this one.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface DoubleInterface
{
}

View File

@@ -0,0 +1,146 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler;
use Doctrine\Instantiator\Instantiator;
use Prophecy\Doubler\ClassPatch\ClassPatchInterface;
use Prophecy\Doubler\Generator\ClassMirror;
use Prophecy\Doubler\Generator\ClassCreator;
use Prophecy\Exception\InvalidArgumentException;
use ReflectionClass;
/**
* Cached class doubler.
* Prevents mirroring/creation of the same structure twice.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class Doubler
{
private $mirror;
private $creator;
private $namer;
/**
* @var ClassPatchInterface[]
*/
private $patches = array();
/**
* @var \Doctrine\Instantiator\Instantiator
*/
private $instantiator;
/**
* Initializes doubler.
*
* @param ClassMirror $mirror
* @param ClassCreator $creator
* @param NameGenerator $namer
*/
public function __construct(ClassMirror $mirror = null, ClassCreator $creator = null,
NameGenerator $namer = null)
{
$this->mirror = $mirror ?: new ClassMirror;
$this->creator = $creator ?: new ClassCreator;
$this->namer = $namer ?: new NameGenerator;
}
/**
* Returns list of registered class patches.
*
* @return ClassPatchInterface[]
*/
public function getClassPatches()
{
return $this->patches;
}
/**
* Registers new class patch.
*
* @param ClassPatchInterface $patch
*/
public function registerClassPatch(ClassPatchInterface $patch)
{
$this->patches[] = $patch;
@usort($this->patches, function (ClassPatchInterface $patch1, ClassPatchInterface $patch2) {
return $patch2->getPriority() - $patch1->getPriority();
});
}
/**
* Creates double from specific class or/and list of interfaces.
*
* @param ReflectionClass $class
* @param ReflectionClass[] $interfaces Array of ReflectionClass instances
* @param array $args Constructor arguments
*
* @return DoubleInterface
*
* @throws \Prophecy\Exception\InvalidArgumentException
*/
public function double(ReflectionClass $class = null, array $interfaces, array $args = null)
{
foreach ($interfaces as $interface) {
if (!$interface instanceof ReflectionClass) {
throw new InvalidArgumentException(sprintf(
"[ReflectionClass \$interface1 [, ReflectionClass \$interface2]] array expected as\n".
"a second argument to `Doubler::double(...)`, but got %s.",
is_object($interface) ? get_class($interface).' class' : gettype($interface)
));
}
}
$classname = $this->createDoubleClass($class, $interfaces);
$reflection = new ReflectionClass($classname);
if (null !== $args) {
return $reflection->newInstanceArgs($args);
}
if ((null === $constructor = $reflection->getConstructor())
|| ($constructor->isPublic() && !$constructor->isFinal())) {
return $reflection->newInstance();
}
if (!$this->instantiator) {
$this->instantiator = new Instantiator();
}
return $this->instantiator->instantiate($classname);
}
/**
* Creates double class and returns its FQN.
*
* @param ReflectionClass $class
* @param ReflectionClass[] $interfaces
*
* @return string
*/
protected function createDoubleClass(ReflectionClass $class = null, array $interfaces)
{
$name = $this->namer->name($class, $interfaces);
$node = $this->mirror->reflect($class, $interfaces);
foreach ($this->patches as $patch) {
if ($patch->supports($node)) {
$patch->apply($node);
}
}
$this->creator->create($name, $node);
return $name;
}
}

View File

@@ -0,0 +1,129 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\Generator;
/**
* Class code creator.
* Generates PHP code for specific class node tree.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ClassCodeGenerator
{
/**
* @var TypeHintReference
*/
private $typeHintReference;
public function __construct(TypeHintReference $typeHintReference = null)
{
$this->typeHintReference = $typeHintReference ?: new TypeHintReference();
}
/**
* Generates PHP code for class node.
*
* @param string $classname
* @param Node\ClassNode $class
*
* @return string
*/
public function generate($classname, Node\ClassNode $class)
{
$parts = explode('\\', $classname);
$classname = array_pop($parts);
$namespace = implode('\\', $parts);
$code = sprintf("class %s extends \%s implements %s {\n",
$classname, $class->getParentClass(), implode(', ',
array_map(function ($interface) {return '\\'.$interface;}, $class->getInterfaces())
)
);
foreach ($class->getProperties() as $name => $visibility) {
$code .= sprintf("%s \$%s;\n", $visibility, $name);
}
$code .= "\n";
foreach ($class->getMethods() as $method) {
$code .= $this->generateMethod($method)."\n";
}
$code .= "\n}";
return sprintf("namespace %s {\n%s\n}", $namespace, $code);
}
private function generateMethod(Node\MethodNode $method)
{
$php = sprintf("%s %s function %s%s(%s)%s {\n",
$method->getVisibility(),
$method->isStatic() ? 'static' : '',
$method->returnsReference() ? '&':'',
$method->getName(),
implode(', ', $this->generateArguments($method->getArguments())),
$this->getReturnType($method)
);
$php .= $method->getCode()."\n";
return $php.'}';
}
/**
* @return string
*/
private function getReturnType(Node\MethodNode $method)
{
if (version_compare(PHP_VERSION, '7.1', '>=')) {
if ($method->hasReturnType()) {
return $method->hasNullableReturnType()
? sprintf(': ?%s', $method->getReturnType())
: sprintf(': %s', $method->getReturnType());
}
}
if (version_compare(PHP_VERSION, '7.0', '>=')) {
return $method->hasReturnType() && $method->getReturnType() !== 'void'
? sprintf(': %s', $method->getReturnType())
: '';
}
return '';
}
private function generateArguments(array $arguments)
{
$typeHintReference = $this->typeHintReference;
return array_map(function (Node\ArgumentNode $argument) use ($typeHintReference) {
$php = '';
if (version_compare(PHP_VERSION, '7.1', '>=')) {
$php .= $argument->isNullable() ? '?' : '';
}
if ($hint = $argument->getTypeHint()) {
$php .= $typeHintReference->isBuiltInParamTypeHint($hint) ? $hint : '\\'.$hint;
}
$php .= ' '.($argument->isPassedByReference() ? '&' : '');
$php .= $argument->isVariadic() ? '...' : '';
$php .= '$'.$argument->getName();
if ($argument->isOptional() && !$argument->isVariadic()) {
$php .= ' = '.var_export($argument->getDefault(), true);
}
return $php;
}, $arguments);
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\Generator;
use Prophecy\Exception\Doubler\ClassCreatorException;
/**
* Class creator.
* Creates specific class in current environment.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ClassCreator
{
private $generator;
/**
* Initializes creator.
*
* @param ClassCodeGenerator $generator
*/
public function __construct(ClassCodeGenerator $generator = null)
{
$this->generator = $generator ?: new ClassCodeGenerator;
}
/**
* Creates class.
*
* @param string $classname
* @param Node\ClassNode $class
*
* @return mixed
*
* @throws \Prophecy\Exception\Doubler\ClassCreatorException
*/
public function create($classname, Node\ClassNode $class)
{
$code = $this->generator->generate($classname, $class);
$return = eval($code);
if (!class_exists($classname, false)) {
if (count($class->getInterfaces())) {
throw new ClassCreatorException(sprintf(
'Could not double `%s` and implement interfaces: [%s].',
$class->getParentClass(), implode(', ', $class->getInterfaces())
), $class);
}
throw new ClassCreatorException(
sprintf('Could not double `%s`.', $class->getParentClass()),
$class
);
}
return $return;
}
}

View File

@@ -0,0 +1,260 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\Generator;
use Prophecy\Exception\InvalidArgumentException;
use Prophecy\Exception\Doubler\ClassMirrorException;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
/**
* Class mirror.
* Core doubler class. Mirrors specific class and/or interfaces into class node tree.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ClassMirror
{
private static $reflectableMethods = array(
'__construct',
'__destruct',
'__sleep',
'__wakeup',
'__toString',
'__call',
'__invoke'
);
/**
* Reflects provided arguments into class node.
*
* @param ReflectionClass $class
* @param ReflectionClass[] $interfaces
*
* @return Node\ClassNode
*
* @throws \Prophecy\Exception\InvalidArgumentException
*/
public function reflect(ReflectionClass $class = null, array $interfaces)
{
$node = new Node\ClassNode;
if (null !== $class) {
if (true === $class->isInterface()) {
throw new InvalidArgumentException(sprintf(
"Could not reflect %s as a class, because it\n".
"is interface - use the second argument instead.",
$class->getName()
));
}
$this->reflectClassToNode($class, $node);
}
foreach ($interfaces as $interface) {
if (!$interface instanceof ReflectionClass) {
throw new InvalidArgumentException(sprintf(
"[ReflectionClass \$interface1 [, ReflectionClass \$interface2]] array expected as\n".
"a second argument to `ClassMirror::reflect(...)`, but got %s.",
is_object($interface) ? get_class($interface).' class' : gettype($interface)
));
}
if (false === $interface->isInterface()) {
throw new InvalidArgumentException(sprintf(
"Could not reflect %s as an interface, because it\n".
"is class - use the first argument instead.",
$interface->getName()
));
}
$this->reflectInterfaceToNode($interface, $node);
}
$node->addInterface('Prophecy\Doubler\Generator\ReflectionInterface');
return $node;
}
private function reflectClassToNode(ReflectionClass $class, Node\ClassNode $node)
{
if (true === $class->isFinal()) {
throw new ClassMirrorException(sprintf(
'Could not reflect class %s as it is marked final.', $class->getName()
), $class);
}
$node->setParentClass($class->getName());
foreach ($class->getMethods(ReflectionMethod::IS_ABSTRACT) as $method) {
if (false === $method->isProtected()) {
continue;
}
$this->reflectMethodToNode($method, $node);
}
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
if (0 === strpos($method->getName(), '_')
&& !in_array($method->getName(), self::$reflectableMethods)) {
continue;
}
if (true === $method->isFinal()) {
$node->addUnextendableMethod($method->getName());
continue;
}
$this->reflectMethodToNode($method, $node);
}
}
private function reflectInterfaceToNode(ReflectionClass $interface, Node\ClassNode $node)
{
$node->addInterface($interface->getName());
foreach ($interface->getMethods() as $method) {
$this->reflectMethodToNode($method, $node);
}
}
private function reflectMethodToNode(ReflectionMethod $method, Node\ClassNode $classNode)
{
$node = new Node\MethodNode($method->getName());
if (true === $method->isProtected()) {
$node->setVisibility('protected');
}
if (true === $method->isStatic()) {
$node->setStatic();
}
if (true === $method->returnsReference()) {
$node->setReturnsReference();
}
if (version_compare(PHP_VERSION, '7.0', '>=') && $method->hasReturnType()) {
$returnType = (string) $method->getReturnType();
$returnTypeLower = strtolower($returnType);
if ('self' === $returnTypeLower) {
$returnType = $method->getDeclaringClass()->getName();
}
if ('parent' === $returnTypeLower) {
$returnType = $method->getDeclaringClass()->getParentClass()->getName();
}
$node->setReturnType($returnType);
if (version_compare(PHP_VERSION, '7.1', '>=') && $method->getReturnType()->allowsNull()) {
$node->setNullableReturnType(true);
}
}
if (is_array($params = $method->getParameters()) && count($params)) {
foreach ($params as $param) {
$this->reflectArgumentToNode($param, $node);
}
}
$classNode->addMethod($node);
}
private function reflectArgumentToNode(ReflectionParameter $parameter, Node\MethodNode $methodNode)
{
$name = $parameter->getName() == '...' ? '__dot_dot_dot__' : $parameter->getName();
$node = new Node\ArgumentNode($name);
$node->setTypeHint($this->getTypeHint($parameter));
if ($this->isVariadic($parameter)) {
$node->setAsVariadic();
}
if ($this->hasDefaultValue($parameter)) {
$node->setDefault($this->getDefaultValue($parameter));
}
if ($parameter->isPassedByReference()) {
$node->setAsPassedByReference();
}
$node->setAsNullable($this->isNullable($parameter));
$methodNode->addArgument($node);
}
private function hasDefaultValue(ReflectionParameter $parameter)
{
if ($this->isVariadic($parameter)) {
return false;
}
if ($parameter->isDefaultValueAvailable()) {
return true;
}
return $parameter->isOptional() || $this->isNullable($parameter);
}
private function getDefaultValue(ReflectionParameter $parameter)
{
if (!$parameter->isDefaultValueAvailable()) {
return null;
}
return $parameter->getDefaultValue();
}
private function getTypeHint(ReflectionParameter $parameter)
{
if (null !== $className = $this->getParameterClassName($parameter)) {
return $className;
}
if (true === $parameter->isArray()) {
return 'array';
}
if (version_compare(PHP_VERSION, '5.4', '>=') && true === $parameter->isCallable()) {
return 'callable';
}
if (version_compare(PHP_VERSION, '7.0', '>=') && true === $parameter->hasType()) {
return (string) $parameter->getType();
}
return null;
}
private function isVariadic(ReflectionParameter $parameter)
{
return PHP_VERSION_ID >= 50600 && $parameter->isVariadic();
}
private function isNullable(ReflectionParameter $parameter)
{
return $parameter->allowsNull() && null !== $this->getTypeHint($parameter);
}
private function getParameterClassName(ReflectionParameter $parameter)
{
try {
return $parameter->getClass() ? $parameter->getClass()->getName() : null;
} catch (\ReflectionException $e) {
preg_match('/\[\s\<\w+?>\s([\w,\\\]+)/s', $parameter, $matches);
return isset($matches[1]) ? $matches[1] : null;
}
}
}

View File

@@ -0,0 +1,102 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\Generator\Node;
/**
* Argument node.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ArgumentNode
{
private $name;
private $typeHint;
private $default;
private $optional = false;
private $byReference = false;
private $isVariadic = false;
private $isNullable = false;
/**
* @param string $name
*/
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function getTypeHint()
{
return $this->typeHint;
}
public function setTypeHint($typeHint = null)
{
$this->typeHint = $typeHint;
}
public function hasDefault()
{
return $this->isOptional() && !$this->isVariadic();
}
public function getDefault()
{
return $this->default;
}
public function setDefault($default = null)
{
$this->optional = true;
$this->default = $default;
}
public function isOptional()
{
return $this->optional;
}
public function setAsPassedByReference($byReference = true)
{
$this->byReference = $byReference;
}
public function isPassedByReference()
{
return $this->byReference;
}
public function setAsVariadic($isVariadic = true)
{
$this->isVariadic = $isVariadic;
}
public function isVariadic()
{
return $this->isVariadic;
}
public function isNullable()
{
return $this->isNullable;
}
public function setAsNullable($isNullable = true)
{
$this->isNullable = $isNullable;
}
}

View File

@@ -0,0 +1,169 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\Generator\Node;
use Prophecy\Exception\Doubler\MethodNotExtendableException;
use Prophecy\Exception\InvalidArgumentException;
/**
* Class node.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ClassNode
{
private $parentClass = 'stdClass';
private $interfaces = array();
private $properties = array();
private $unextendableMethods = array();
/**
* @var MethodNode[]
*/
private $methods = array();
public function getParentClass()
{
return $this->parentClass;
}
/**
* @param string $class
*/
public function setParentClass($class)
{
$this->parentClass = $class ?: 'stdClass';
}
/**
* @return string[]
*/
public function getInterfaces()
{
return $this->interfaces;
}
/**
* @param string $interface
*/
public function addInterface($interface)
{
if ($this->hasInterface($interface)) {
return;
}
array_unshift($this->interfaces, $interface);
}
/**
* @param string $interface
*
* @return bool
*/
public function hasInterface($interface)
{
return in_array($interface, $this->interfaces);
}
public function getProperties()
{
return $this->properties;
}
public function addProperty($name, $visibility = 'public')
{
$visibility = strtolower($visibility);
if (!in_array($visibility, array('public', 'private', 'protected'))) {
throw new InvalidArgumentException(sprintf(
'`%s` property visibility is not supported.', $visibility
));
}
$this->properties[$name] = $visibility;
}
/**
* @return MethodNode[]
*/
public function getMethods()
{
return $this->methods;
}
public function addMethod(MethodNode $method, $force = false)
{
if (!$this->isExtendable($method->getName())){
$message = sprintf(
'Method `%s` is not extendable, so can not be added.', $method->getName()
);
throw new MethodNotExtendableException($message, $this->getParentClass(), $method->getName());
}
if ($force || !isset($this->methods[$method->getName()])) {
$this->methods[$method->getName()] = $method;
}
}
public function removeMethod($name)
{
unset($this->methods[$name]);
}
/**
* @param string $name
*
* @return MethodNode|null
*/
public function getMethod($name)
{
return $this->hasMethod($name) ? $this->methods[$name] : null;
}
/**
* @param string $name
*
* @return bool
*/
public function hasMethod($name)
{
return isset($this->methods[$name]);
}
/**
* @return string[]
*/
public function getUnextendableMethods()
{
return $this->unextendableMethods;
}
/**
* @param string $unextendableMethod
*/
public function addUnextendableMethod($unextendableMethod)
{
if (!$this->isExtendable($unextendableMethod)){
return;
}
$this->unextendableMethods[] = $unextendableMethod;
}
/**
* @param string $method
* @return bool
*/
public function isExtendable($method)
{
return !in_array($method, $this->unextendableMethods);
}
}

View File

@@ -0,0 +1,198 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\Generator\Node;
use Prophecy\Doubler\Generator\TypeHintReference;
use Prophecy\Exception\InvalidArgumentException;
/**
* Method node.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class MethodNode
{
private $name;
private $code;
private $visibility = 'public';
private $static = false;
private $returnsReference = false;
private $returnType;
private $nullableReturnType = false;
/**
* @var ArgumentNode[]
*/
private $arguments = array();
/**
* @var TypeHintReference
*/
private $typeHintReference;
/**
* @param string $name
* @param string $code
*/
public function __construct($name, $code = null, TypeHintReference $typeHintReference = null)
{
$this->name = $name;
$this->code = $code;
$this->typeHintReference = $typeHintReference ?: new TypeHintReference();
}
public function getVisibility()
{
return $this->visibility;
}
/**
* @param string $visibility
*/
public function setVisibility($visibility)
{
$visibility = strtolower($visibility);
if (!in_array($visibility, array('public', 'private', 'protected'))) {
throw new InvalidArgumentException(sprintf(
'`%s` method visibility is not supported.', $visibility
));
}
$this->visibility = $visibility;
}
public function isStatic()
{
return $this->static;
}
public function setStatic($static = true)
{
$this->static = (bool) $static;
}
public function returnsReference()
{
return $this->returnsReference;
}
public function setReturnsReference()
{
$this->returnsReference = true;
}
public function getName()
{
return $this->name;
}
public function addArgument(ArgumentNode $argument)
{
$this->arguments[] = $argument;
}
/**
* @return ArgumentNode[]
*/
public function getArguments()
{
return $this->arguments;
}
public function hasReturnType()
{
return null !== $this->returnType;
}
/**
* @param string $type
*/
public function setReturnType($type = null)
{
if ($type === '' || $type === null) {
$this->returnType = null;
return;
}
$typeMap = array(
'double' => 'float',
'real' => 'float',
'boolean' => 'bool',
'integer' => 'int',
);
if (isset($typeMap[$type])) {
$type = $typeMap[$type];
}
$this->returnType = $this->typeHintReference->isBuiltInReturnTypeHint($type) ?
$type :
'\\' . ltrim($type, '\\');
}
public function getReturnType()
{
return $this->returnType;
}
/**
* @param bool $bool
*/
public function setNullableReturnType($bool = true)
{
$this->nullableReturnType = (bool) $bool;
}
/**
* @return bool
*/
public function hasNullableReturnType()
{
return $this->nullableReturnType;
}
/**
* @param string $code
*/
public function setCode($code)
{
$this->code = $code;
}
public function getCode()
{
if ($this->returnsReference)
{
return "throw new \Prophecy\Exception\Doubler\ReturnByReferenceException('Returning by reference not supported', get_class(\$this), '{$this->name}');";
}
return (string) $this->code;
}
public function useParentCode()
{
$this->code = sprintf(
'return parent::%s(%s);', $this->getName(), implode(', ',
array_map(array($this, 'generateArgument'), $this->arguments)
)
);
}
private function generateArgument(ArgumentNode $arg)
{
$argument = '$'.$arg->getName();
if ($arg->isVariadic()) {
$argument = '...'.$argument;
}
return $argument;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\Generator;
/**
* Reflection interface.
* All reflected classes implement this interface.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface ReflectionInterface
{
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Prophecy\Doubler\Generator;
/**
* Tells whether a keyword refers to a class or to a built-in type for the
* current version of php
*/
final class TypeHintReference
{
public function isBuiltInParamTypeHint($type)
{
switch ($type) {
case 'self':
case 'array':
return true;
case 'callable':
return PHP_VERSION_ID >= 50400;
case 'bool':
case 'float':
case 'int':
case 'string':
return PHP_VERSION_ID >= 70000;
case 'iterable':
return PHP_VERSION_ID >= 70100;
case 'object':
return PHP_VERSION_ID >= 70200;
default:
return false;
}
}
public function isBuiltInReturnTypeHint($type)
{
if ($type === 'void') {
return PHP_VERSION_ID >= 70100;
}
return $this->isBuiltInParamTypeHint($type);
}
}

View File

@@ -0,0 +1,127 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler;
use Prophecy\Exception\Doubler\DoubleException;
use Prophecy\Exception\Doubler\ClassNotFoundException;
use Prophecy\Exception\Doubler\InterfaceNotFoundException;
use ReflectionClass;
/**
* Lazy double.
* Gives simple interface to describe double before creating it.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class LazyDouble
{
private $doubler;
private $class;
private $interfaces = array();
private $arguments = null;
private $double;
/**
* Initializes lazy double.
*
* @param Doubler $doubler
*/
public function __construct(Doubler $doubler)
{
$this->doubler = $doubler;
}
/**
* Tells doubler to use specific class as parent one for double.
*
* @param string|ReflectionClass $class
*
* @throws \Prophecy\Exception\Doubler\ClassNotFoundException
* @throws \Prophecy\Exception\Doubler\DoubleException
*/
public function setParentClass($class)
{
if (null !== $this->double) {
throw new DoubleException('Can not extend class with already instantiated double.');
}
if (!$class instanceof ReflectionClass) {
if (!class_exists($class)) {
throw new ClassNotFoundException(sprintf('Class %s not found.', $class), $class);
}
$class = new ReflectionClass($class);
}
$this->class = $class;
}
/**
* Tells doubler to implement specific interface with double.
*
* @param string|ReflectionClass $interface
*
* @throws \Prophecy\Exception\Doubler\InterfaceNotFoundException
* @throws \Prophecy\Exception\Doubler\DoubleException
*/
public function addInterface($interface)
{
if (null !== $this->double) {
throw new DoubleException(
'Can not implement interface with already instantiated double.'
);
}
if (!$interface instanceof ReflectionClass) {
if (!interface_exists($interface)) {
throw new InterfaceNotFoundException(
sprintf('Interface %s not found.', $interface),
$interface
);
}
$interface = new ReflectionClass($interface);
}
$this->interfaces[] = $interface;
}
/**
* Sets constructor arguments.
*
* @param array $arguments
*/
public function setArguments(array $arguments = null)
{
$this->arguments = $arguments;
}
/**
* Creates double instance or returns already created one.
*
* @return DoubleInterface
*/
public function getInstance()
{
if (null === $this->double) {
if (null !== $this->arguments) {
return $this->double = $this->doubler->double(
$this->class, $this->interfaces, $this->arguments
);
}
$this->double = $this->doubler->double($this->class, $this->interfaces);
}
return $this->double;
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler;
use ReflectionClass;
/**
* Name generator.
* Generates classname for double.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class NameGenerator
{
private static $counter = 1;
/**
* Generates name.
*
* @param ReflectionClass $class
* @param ReflectionClass[] $interfaces
*
* @return string
*/
public function name(ReflectionClass $class = null, array $interfaces)
{
$parts = array();
if (null !== $class) {
$parts[] = $class->getName();
} else {
foreach ($interfaces as $interface) {
$parts[] = $interface->getShortName();
}
}
if (!count($parts)) {
$parts[] = 'stdClass';
}
return sprintf('Double\%s\P%d', implode('\\', $parts), self::$counter++);
}
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Call;
use Prophecy\Exception\Prophecy\ObjectProphecyException;
use Prophecy\Prophecy\ObjectProphecy;
class UnexpectedCallException extends ObjectProphecyException
{
private $methodName;
private $arguments;
public function __construct($message, ObjectProphecy $objectProphecy,
$methodName, array $arguments)
{
parent::__construct($message, $objectProphecy);
$this->methodName = $methodName;
$this->arguments = $arguments;
}
public function getMethodName()
{
return $this->methodName;
}
public function getArguments()
{
return $this->arguments;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Doubler;
use Prophecy\Doubler\Generator\Node\ClassNode;
class ClassCreatorException extends \RuntimeException implements DoublerException
{
private $node;
public function __construct($message, ClassNode $node)
{
parent::__construct($message);
$this->node = $node;
}
public function getClassNode()
{
return $this->node;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Doubler;
use ReflectionClass;
class ClassMirrorException extends \RuntimeException implements DoublerException
{
private $class;
public function __construct($message, ReflectionClass $class)
{
parent::__construct($message);
$this->class = $class;
}
public function getReflectedClass()
{
return $this->class;
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Doubler;
class ClassNotFoundException extends DoubleException
{
private $classname;
/**
* @param string $message
* @param string $classname
*/
public function __construct($message, $classname)
{
parent::__construct($message);
$this->classname = $classname;
}
public function getClassname()
{
return $this->classname;
}
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Doubler;
use RuntimeException;
class DoubleException extends RuntimeException implements DoublerException
{
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Doubler;
use Prophecy\Exception\Exception;
interface DoublerException extends Exception
{
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Doubler;
class InterfaceNotFoundException extends ClassNotFoundException
{
public function getInterfaceName()
{
return $this->getClassname();
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Prophecy\Exception\Doubler;
class MethodNotExtendableException extends DoubleException
{
private $methodName;
private $className;
/**
* @param string $message
* @param string $className
* @param string $methodName
*/
public function __construct($message, $className, $methodName)
{
parent::__construct($message);
$this->methodName = $methodName;
$this->className = $className;
}
/**
* @return string
*/
public function getMethodName()
{
return $this->methodName;
}
/**
* @return string
*/
public function getClassName()
{
return $this->className;
}
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Doubler;
class MethodNotFoundException extends DoubleException
{
/**
* @var string|object
*/
private $classname;
/**
* @var string
*/
private $methodName;
/**
* @var array
*/
private $arguments;
/**
* @param string $message
* @param string|object $classname
* @param string $methodName
* @param null|Argument\ArgumentsWildcard|array $arguments
*/
public function __construct($message, $classname, $methodName, $arguments = null)
{
parent::__construct($message);
$this->classname = $classname;
$this->methodName = $methodName;
$this->arguments = $arguments;
}
public function getClassname()
{
return $this->classname;
}
public function getMethodName()
{
return $this->methodName;
}
public function getArguments()
{
return $this->arguments;
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Doubler;
class ReturnByReferenceException extends DoubleException
{
private $classname;
private $methodName;
/**
* @param string $message
* @param string $classname
* @param string $methodName
*/
public function __construct($message, $classname, $methodName)
{
parent::__construct($message);
$this->classname = $classname;
$this->methodName = $methodName;
}
public function getClassname()
{
return $this->classname;
}
public function getMethodName()
{
return $this->methodName;
}
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception;
/**
* Core Prophecy exception interface.
* All Prophecy exceptions implement it.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface Exception
{
/**
* @return string
*/
public function getMessage();
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements Exception
{
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Prediction;
use Prophecy\Prophecy\ObjectProphecy;
class AggregateException extends \RuntimeException implements PredictionException
{
private $exceptions = array();
private $objectProphecy;
public function append(PredictionException $exception)
{
$message = $exception->getMessage();
$message = strtr($message, array("\n" => "\n "))."\n";
$message = empty($this->exceptions) ? $message : "\n" . $message;
$this->message = rtrim($this->message.$message);
$this->exceptions[] = $exception;
}
/**
* @return PredictionException[]
*/
public function getExceptions()
{
return $this->exceptions;
}
public function setObjectProphecy(ObjectProphecy $objectProphecy)
{
$this->objectProphecy = $objectProphecy;
}
/**
* @return ObjectProphecy
*/
public function getObjectProphecy()
{
return $this->objectProphecy;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Prediction;
use RuntimeException;
/**
* Basic failed prediction exception.
* Use it for custom prediction failures.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class FailedPredictionException extends RuntimeException implements PredictionException
{
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Prediction;
use Prophecy\Exception\Prophecy\MethodProphecyException;
class NoCallsException extends MethodProphecyException implements PredictionException
{
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Prediction;
use Prophecy\Exception\Exception;
interface PredictionException extends Exception
{
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Prediction;
use Prophecy\Prophecy\MethodProphecy;
class UnexpectedCallsCountException extends UnexpectedCallsException
{
private $expectedCount;
public function __construct($message, MethodProphecy $methodProphecy, $count, array $calls)
{
parent::__construct($message, $methodProphecy, $calls);
$this->expectedCount = intval($count);
}
public function getExpectedCount()
{
return $this->expectedCount;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Prediction;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Exception\Prophecy\MethodProphecyException;
class UnexpectedCallsException extends MethodProphecyException implements PredictionException
{
private $calls = array();
public function __construct($message, MethodProphecy $methodProphecy, array $calls)
{
parent::__construct($message, $methodProphecy);
$this->calls = $calls;
}
public function getCalls()
{
return $this->calls;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Prophecy;
use Prophecy\Prophecy\MethodProphecy;
class MethodProphecyException extends ObjectProphecyException
{
private $methodProphecy;
public function __construct($message, MethodProphecy $methodProphecy)
{
parent::__construct($message, $methodProphecy->getObjectProphecy());
$this->methodProphecy = $methodProphecy;
}
/**
* @return MethodProphecy
*/
public function getMethodProphecy()
{
return $this->methodProphecy;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Prophecy;
use Prophecy\Prophecy\ObjectProphecy;
class ObjectProphecyException extends \RuntimeException implements ProphecyException
{
private $objectProphecy;
public function __construct($message, ObjectProphecy $objectProphecy)
{
parent::__construct($message);
$this->objectProphecy = $objectProphecy;
}
/**
* @return ObjectProphecy
*/
public function getObjectProphecy()
{
return $this->objectProphecy;
}
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Exception\Prophecy;
use Prophecy\Exception\Exception;
interface ProphecyException extends Exception
{
}

View File

@@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\PhpDocumentor;
use phpDocumentor\Reflection\DocBlock\Tag\MethodTag as LegacyMethodTag;
use phpDocumentor\Reflection\DocBlock\Tags\Method;
/**
* @author Théo FIDRY <theo.fidry@gmail.com>
*
* @internal
*/
final class ClassAndInterfaceTagRetriever implements MethodTagRetrieverInterface
{
private $classRetriever;
public function __construct(MethodTagRetrieverInterface $classRetriever = null)
{
if (null !== $classRetriever) {
$this->classRetriever = $classRetriever;
return;
}
$this->classRetriever = class_exists('phpDocumentor\Reflection\DocBlockFactory') && class_exists('phpDocumentor\Reflection\Types\ContextFactory')
? new ClassTagRetriever()
: new LegacyClassTagRetriever()
;
}
/**
* @param \ReflectionClass $reflectionClass
*
* @return LegacyMethodTag[]|Method[]
*/
public function getTagList(\ReflectionClass $reflectionClass)
{
return array_merge(
$this->classRetriever->getTagList($reflectionClass),
$this->getInterfacesTagList($reflectionClass)
);
}
/**
* @param \ReflectionClass $reflectionClass
*
* @return LegacyMethodTag[]|Method[]
*/
private function getInterfacesTagList(\ReflectionClass $reflectionClass)
{
$interfaces = $reflectionClass->getInterfaces();
$tagList = array();
foreach($interfaces as $interface) {
$tagList = array_merge($tagList, $this->classRetriever->getTagList($interface));
}
return $tagList;
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\PhpDocumentor;
use phpDocumentor\Reflection\DocBlock\Tags\Method;
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\Types\ContextFactory;
/**
* @author Théo FIDRY <theo.fidry@gmail.com>
*
* @internal
*/
final class ClassTagRetriever implements MethodTagRetrieverInterface
{
private $docBlockFactory;
private $contextFactory;
public function __construct()
{
$this->docBlockFactory = DocBlockFactory::createInstance();
$this->contextFactory = new ContextFactory();
}
/**
* @param \ReflectionClass $reflectionClass
*
* @return Method[]
*/
public function getTagList(\ReflectionClass $reflectionClass)
{
try {
$phpdoc = $this->docBlockFactory->create(
$reflectionClass,
$this->contextFactory->createFromReflector($reflectionClass)
);
return $phpdoc->getTagsByName('method');
} catch (\InvalidArgumentException $e) {
return array();
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\PhpDocumentor;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlock\Tag\MethodTag as LegacyMethodTag;
/**
* @author Théo FIDRY <theo.fidry@gmail.com>
*
* @internal
*/
final class LegacyClassTagRetriever implements MethodTagRetrieverInterface
{
/**
* @param \ReflectionClass $reflectionClass
*
* @return LegacyMethodTag[]
*/
public function getTagList(\ReflectionClass $reflectionClass)
{
$phpdoc = new DocBlock($reflectionClass->getDocComment());
return $phpdoc->getTagsByName('method');
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\PhpDocumentor;
use phpDocumentor\Reflection\DocBlock\Tag\MethodTag as LegacyMethodTag;
use phpDocumentor\Reflection\DocBlock\Tags\Method;
/**
* @author Théo FIDRY <theo.fidry@gmail.com>
*
* @internal
*/
interface MethodTagRetrieverInterface
{
/**
* @param \ReflectionClass $reflectionClass
*
* @return LegacyMethodTag[]|Method[]
*/
public function getTagList(\ReflectionClass $reflectionClass);
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Prediction;
use Prophecy\Call\Call;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Argument\ArgumentsWildcard;
use Prophecy\Argument\Token\AnyValuesToken;
use Prophecy\Util\StringUtil;
use Prophecy\Exception\Prediction\NoCallsException;
/**
* Call prediction.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class CallPrediction implements PredictionInterface
{
private $util;
/**
* Initializes prediction.
*
* @param StringUtil $util
*/
public function __construct(StringUtil $util = null)
{
$this->util = $util ?: new StringUtil;
}
/**
* Tests that there was at least one call.
*
* @param Call[] $calls
* @param ObjectProphecy $object
* @param MethodProphecy $method
*
* @throws \Prophecy\Exception\Prediction\NoCallsException
*/
public function check(array $calls, ObjectProphecy $object, MethodProphecy $method)
{
if (count($calls)) {
return;
}
$methodCalls = $object->findProphecyMethodCalls(
$method->getMethodName(),
new ArgumentsWildcard(array(new AnyValuesToken))
);
if (count($methodCalls)) {
throw new NoCallsException(sprintf(
"No calls have been made that match:\n".
" %s->%s(%s)\n".
"but expected at least one.\n".
"Recorded `%s(...)` calls:\n%s",
get_class($object->reveal()),
$method->getMethodName(),
$method->getArgumentsWildcard(),
$method->getMethodName(),
$this->util->stringifyCalls($methodCalls)
), $method);
}
throw new NoCallsException(sprintf(
"No calls have been made that match:\n".
" %s->%s(%s)\n".
"but expected at least one.",
get_class($object->reveal()),
$method->getMethodName(),
$method->getArgumentsWildcard()
), $method);
}
}

View File

@@ -0,0 +1,107 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Prediction;
use Prophecy\Call\Call;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Argument\ArgumentsWildcard;
use Prophecy\Argument\Token\AnyValuesToken;
use Prophecy\Util\StringUtil;
use Prophecy\Exception\Prediction\UnexpectedCallsCountException;
/**
* Prediction interface.
* Predictions are logical test blocks, tied to `should...` keyword.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class CallTimesPrediction implements PredictionInterface
{
private $times;
private $util;
/**
* Initializes prediction.
*
* @param int $times
* @param StringUtil $util
*/
public function __construct($times, StringUtil $util = null)
{
$this->times = intval($times);
$this->util = $util ?: new StringUtil;
}
/**
* Tests that there was exact amount of calls made.
*
* @param Call[] $calls
* @param ObjectProphecy $object
* @param MethodProphecy $method
*
* @throws \Prophecy\Exception\Prediction\UnexpectedCallsCountException
*/
public function check(array $calls, ObjectProphecy $object, MethodProphecy $method)
{
if ($this->times == count($calls)) {
return;
}
$methodCalls = $object->findProphecyMethodCalls(
$method->getMethodName(),
new ArgumentsWildcard(array(new AnyValuesToken))
);
if (count($calls)) {
$message = sprintf(
"Expected exactly %d calls that match:\n".
" %s->%s(%s)\n".
"but %d were made:\n%s",
$this->times,
get_class($object->reveal()),
$method->getMethodName(),
$method->getArgumentsWildcard(),
count($calls),
$this->util->stringifyCalls($calls)
);
} elseif (count($methodCalls)) {
$message = sprintf(
"Expected exactly %d calls that match:\n".
" %s->%s(%s)\n".
"but none were made.\n".
"Recorded `%s(...)` calls:\n%s",
$this->times,
get_class($object->reveal()),
$method->getMethodName(),
$method->getArgumentsWildcard(),
$method->getMethodName(),
$this->util->stringifyCalls($methodCalls)
);
} else {
$message = sprintf(
"Expected exactly %d calls that match:\n".
" %s->%s(%s)\n".
"but none were made.",
$this->times,
get_class($object->reveal()),
$method->getMethodName(),
$method->getArgumentsWildcard()
);
}
throw new UnexpectedCallsCountException($message, $method, $this->times, $calls);
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Prediction;
use Prophecy\Call\Call;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Exception\InvalidArgumentException;
use Closure;
/**
* Callback prediction.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class CallbackPrediction implements PredictionInterface
{
private $callback;
/**
* Initializes callback prediction.
*
* @param callable $callback Custom callback
*
* @throws \Prophecy\Exception\InvalidArgumentException
*/
public function __construct($callback)
{
if (!is_callable($callback)) {
throw new InvalidArgumentException(sprintf(
'Callable expected as an argument to CallbackPrediction, but got %s.',
gettype($callback)
));
}
$this->callback = $callback;
}
/**
* Executes preset callback.
*
* @param Call[] $calls
* @param ObjectProphecy $object
* @param MethodProphecy $method
*/
public function check(array $calls, ObjectProphecy $object, MethodProphecy $method)
{
$callback = $this->callback;
if ($callback instanceof Closure && method_exists('Closure', 'bind')) {
$callback = Closure::bind($callback, $object);
}
call_user_func($callback, $calls, $object, $method);
}
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Prediction;
use Prophecy\Call\Call;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Util\StringUtil;
use Prophecy\Exception\Prediction\UnexpectedCallsException;
/**
* No calls prediction.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class NoCallsPrediction implements PredictionInterface
{
private $util;
/**
* Initializes prediction.
*
* @param null|StringUtil $util
*/
public function __construct(StringUtil $util = null)
{
$this->util = $util ?: new StringUtil;
}
/**
* Tests that there were no calls made.
*
* @param Call[] $calls
* @param ObjectProphecy $object
* @param MethodProphecy $method
*
* @throws \Prophecy\Exception\Prediction\UnexpectedCallsException
*/
public function check(array $calls, ObjectProphecy $object, MethodProphecy $method)
{
if (!count($calls)) {
return;
}
$verb = count($calls) === 1 ? 'was' : 'were';
throw new UnexpectedCallsException(sprintf(
"No calls expected that match:\n".
" %s->%s(%s)\n".
"but %d %s made:\n%s",
get_class($object->reveal()),
$method->getMethodName(),
$method->getArgumentsWildcard(),
count($calls),
$verb,
$this->util->stringifyCalls($calls)
), $method, $calls);
}
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Prediction;
use Prophecy\Call\Call;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\MethodProphecy;
/**
* Prediction interface.
* Predictions are logical test blocks, tied to `should...` keyword.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface PredictionInterface
{
/**
* Tests that double fulfilled prediction.
*
* @param Call[] $calls
* @param ObjectProphecy $object
* @param MethodProphecy $method
*
* @throws object
* @return void
*/
public function check(array $calls, ObjectProphecy $object, MethodProphecy $method);
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Promise;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Exception\InvalidArgumentException;
use Closure;
/**
* Callback promise.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class CallbackPromise implements PromiseInterface
{
private $callback;
/**
* Initializes callback promise.
*
* @param callable $callback Custom callback
*
* @throws \Prophecy\Exception\InvalidArgumentException
*/
public function __construct($callback)
{
if (!is_callable($callback)) {
throw new InvalidArgumentException(sprintf(
'Callable expected as an argument to CallbackPromise, but got %s.',
gettype($callback)
));
}
$this->callback = $callback;
}
/**
* Evaluates promise callback.
*
* @param array $args
* @param ObjectProphecy $object
* @param MethodProphecy $method
*
* @return mixed
*/
public function execute(array $args, ObjectProphecy $object, MethodProphecy $method)
{
$callback = $this->callback;
if ($callback instanceof Closure && method_exists('Closure', 'bind')) {
$callback = Closure::bind($callback, $object);
}
return call_user_func($callback, $args, $object, $method);
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Promise;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\MethodProphecy;
/**
* Promise interface.
* Promises are logical blocks, tied to `will...` keyword.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
interface PromiseInterface
{
/**
* Evaluates promise.
*
* @param array $args
* @param ObjectProphecy $object
* @param MethodProphecy $method
*
* @return mixed
*/
public function execute(array $args, ObjectProphecy $object, MethodProphecy $method);
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Promise;
use Prophecy\Exception\InvalidArgumentException;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\MethodProphecy;
/**
* Return argument promise.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ReturnArgumentPromise implements PromiseInterface
{
/**
* @var int
*/
private $index;
/**
* Initializes callback promise.
*
* @param int $index The zero-indexed number of the argument to return
*
* @throws \Prophecy\Exception\InvalidArgumentException
*/
public function __construct($index = 0)
{
if (!is_int($index) || $index < 0) {
throw new InvalidArgumentException(sprintf(
'Zero-based index expected as argument to ReturnArgumentPromise, but got %s.',
$index
));
}
$this->index = $index;
}
/**
* Returns nth argument if has one, null otherwise.
*
* @param array $args
* @param ObjectProphecy $object
* @param MethodProphecy $method
*
* @return null|mixed
*/
public function execute(array $args, ObjectProphecy $object, MethodProphecy $method)
{
return count($args) > $this->index ? $args[$this->index] : null;
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Promise;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\MethodProphecy;
/**
* Return promise.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ReturnPromise implements PromiseInterface
{
private $returnValues = array();
/**
* Initializes promise.
*
* @param array $returnValues Array of values
*/
public function __construct(array $returnValues)
{
$this->returnValues = $returnValues;
}
/**
* Returns saved values one by one until last one, then continuously returns last value.
*
* @param array $args
* @param ObjectProphecy $object
* @param MethodProphecy $method
*
* @return mixed
*/
public function execute(array $args, ObjectProphecy $object, MethodProphecy $method)
{
$value = array_shift($this->returnValues);
if (!count($this->returnValues)) {
$this->returnValues[] = $value;
}
return $value;
}
}

View File

@@ -0,0 +1,99 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Promise;
use Doctrine\Instantiator\Instantiator;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Exception\InvalidArgumentException;
use ReflectionClass;
/**
* Throw promise.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ThrowPromise implements PromiseInterface
{
private $exception;
/**
* @var \Doctrine\Instantiator\Instantiator
*/
private $instantiator;
/**
* Initializes promise.
*
* @param string|\Exception|\Throwable $exception Exception class name or instance
*
* @throws \Prophecy\Exception\InvalidArgumentException
*/
public function __construct($exception)
{
if (is_string($exception)) {
if (!class_exists($exception) || !$this->isAValidThrowable($exception)) {
throw new InvalidArgumentException(sprintf(
'Exception / Throwable class or instance expected as argument to ThrowPromise, but got %s.',
$exception
));
}
} elseif (!$exception instanceof \Exception && !$exception instanceof \Throwable) {
throw new InvalidArgumentException(sprintf(
'Exception / Throwable class or instance expected as argument to ThrowPromise, but got %s.',
is_object($exception) ? get_class($exception) : gettype($exception)
));
}
$this->exception = $exception;
}
/**
* Throws predefined exception.
*
* @param array $args
* @param ObjectProphecy $object
* @param MethodProphecy $method
*
* @throws object
*/
public function execute(array $args, ObjectProphecy $object, MethodProphecy $method)
{
if (is_string($this->exception)) {
$classname = $this->exception;
$reflection = new ReflectionClass($classname);
$constructor = $reflection->getConstructor();
if ($constructor->isPublic() && 0 == $constructor->getNumberOfRequiredParameters()) {
throw $reflection->newInstance();
}
if (!$this->instantiator) {
$this->instantiator = new Instantiator();
}
throw $this->instantiator->instantiate($classname);
}
throw $this->exception;
}
/**
* @param string $exception
*
* @return bool
*/
private function isAValidThrowable($exception)
{
return is_a($exception, 'Exception', true) || is_subclass_of($exception, 'Throwable', true);
}
}

View File

@@ -0,0 +1,488 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Prophecy;
use Prophecy\Argument;
use Prophecy\Prophet;
use Prophecy\Promise;
use Prophecy\Prediction;
use Prophecy\Exception\Doubler\MethodNotFoundException;
use Prophecy\Exception\InvalidArgumentException;
use Prophecy\Exception\Prophecy\MethodProphecyException;
/**
* Method prophecy.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class MethodProphecy
{
private $objectProphecy;
private $methodName;
private $argumentsWildcard;
private $promise;
private $prediction;
private $checkedPredictions = array();
private $bound = false;
private $voidReturnType = false;
/**
* Initializes method prophecy.
*
* @param ObjectProphecy $objectProphecy
* @param string $methodName
* @param null|Argument\ArgumentsWildcard|array $arguments
*
* @throws \Prophecy\Exception\Doubler\MethodNotFoundException If method not found
*/
public function __construct(ObjectProphecy $objectProphecy, $methodName, $arguments = null)
{
$double = $objectProphecy->reveal();
if (!method_exists($double, $methodName)) {
throw new MethodNotFoundException(sprintf(
'Method `%s::%s()` is not defined.', get_class($double), $methodName
), get_class($double), $methodName, $arguments);
}
$this->objectProphecy = $objectProphecy;
$this->methodName = $methodName;
$reflectedMethod = new \ReflectionMethod($double, $methodName);
if ($reflectedMethod->isFinal()) {
throw new MethodProphecyException(sprintf(
"Can not add prophecy for a method `%s::%s()`\n".
"as it is a final method.",
get_class($double),
$methodName
), $this);
}
if (null !== $arguments) {
$this->withArguments($arguments);
}
if (version_compare(PHP_VERSION, '7.0', '>=') && true === $reflectedMethod->hasReturnType()) {
$type = (string) $reflectedMethod->getReturnType();
if ('void' === $type) {
$this->voidReturnType = true;
}
$this->will(function () use ($type) {
switch ($type) {
case 'void': return;
case 'string': return '';
case 'float': return 0.0;
case 'int': return 0;
case 'bool': return false;
case 'array': return array();
case 'callable':
case 'Closure':
return function () {};
case 'Traversable':
case 'Generator':
// Remove eval() when minimum version >=5.5
/** @var callable $generator */
$generator = eval('return function () { yield; };');
return $generator();
default:
$prophet = new Prophet;
return $prophet->prophesize($type)->reveal();
}
});
}
}
/**
* Sets argument wildcard.
*
* @param array|Argument\ArgumentsWildcard $arguments
*
* @return $this
*
* @throws \Prophecy\Exception\InvalidArgumentException
*/
public function withArguments($arguments)
{
if (is_array($arguments)) {
$arguments = new Argument\ArgumentsWildcard($arguments);
}
if (!$arguments instanceof Argument\ArgumentsWildcard) {
throw new InvalidArgumentException(sprintf(
"Either an array or an instance of ArgumentsWildcard expected as\n".
'a `MethodProphecy::withArguments()` argument, but got %s.',
gettype($arguments)
));
}
$this->argumentsWildcard = $arguments;
return $this;
}
/**
* Sets custom promise to the prophecy.
*
* @param callable|Promise\PromiseInterface $promise
*
* @return $this
*
* @throws \Prophecy\Exception\InvalidArgumentException
*/
public function will($promise)
{
if (is_callable($promise)) {
$promise = new Promise\CallbackPromise($promise);
}
if (!$promise instanceof Promise\PromiseInterface) {
throw new InvalidArgumentException(sprintf(
'Expected callable or instance of PromiseInterface, but got %s.',
gettype($promise)
));
}
$this->bindToObjectProphecy();
$this->promise = $promise;
return $this;
}
/**
* Sets return promise to the prophecy.
*
* @see \Prophecy\Promise\ReturnPromise
*
* @return $this
*/
public function willReturn()
{
if ($this->voidReturnType) {
throw new MethodProphecyException(
"The method \"$this->methodName\" has a void return type, and so cannot return anything",
$this
);
}
return $this->will(new Promise\ReturnPromise(func_get_args()));
}
/**
* Sets return argument promise to the prophecy.
*
* @param int $index The zero-indexed number of the argument to return
*
* @see \Prophecy\Promise\ReturnArgumentPromise
*
* @return $this
*/
public function willReturnArgument($index = 0)
{
if ($this->voidReturnType) {
throw new MethodProphecyException("The method \"$this->methodName\" has a void return type", $this);
}
return $this->will(new Promise\ReturnArgumentPromise($index));
}
/**
* Sets throw promise to the prophecy.
*
* @see \Prophecy\Promise\ThrowPromise
*
* @param string|\Exception $exception Exception class or instance
*
* @return $this
*/
public function willThrow($exception)
{
return $this->will(new Promise\ThrowPromise($exception));
}
/**
* Sets custom prediction to the prophecy.
*
* @param callable|Prediction\PredictionInterface $prediction
*
* @return $this
*
* @throws \Prophecy\Exception\InvalidArgumentException
*/
public function should($prediction)
{
if (is_callable($prediction)) {
$prediction = new Prediction\CallbackPrediction($prediction);
}
if (!$prediction instanceof Prediction\PredictionInterface) {
throw new InvalidArgumentException(sprintf(
'Expected callable or instance of PredictionInterface, but got %s.',
gettype($prediction)
));
}
$this->bindToObjectProphecy();
$this->prediction = $prediction;
return $this;
}
/**
* Sets call prediction to the prophecy.
*
* @see \Prophecy\Prediction\CallPrediction
*
* @return $this
*/
public function shouldBeCalled()
{
return $this->should(new Prediction\CallPrediction);
}
/**
* Sets no calls prediction to the prophecy.
*
* @see \Prophecy\Prediction\NoCallsPrediction
*
* @return $this
*/
public function shouldNotBeCalled()
{
return $this->should(new Prediction\NoCallsPrediction);
}
/**
* Sets call times prediction to the prophecy.
*
* @see \Prophecy\Prediction\CallTimesPrediction
*
* @param $count
*
* @return $this
*/
public function shouldBeCalledTimes($count)
{
return $this->should(new Prediction\CallTimesPrediction($count));
}
/**
* Sets call times prediction to the prophecy.
*
* @see \Prophecy\Prediction\CallTimesPrediction
*
* @return $this
*/
public function shouldBeCalledOnce()
{
return $this->shouldBeCalledTimes(1);
}
/**
* Checks provided prediction immediately.
*
* @param callable|Prediction\PredictionInterface $prediction
*
* @return $this
*
* @throws \Prophecy\Exception\InvalidArgumentException
*/
public function shouldHave($prediction)
{
if (is_callable($prediction)) {
$prediction = new Prediction\CallbackPrediction($prediction);
}
if (!$prediction instanceof Prediction\PredictionInterface) {
throw new InvalidArgumentException(sprintf(
'Expected callable or instance of PredictionInterface, but got %s.',
gettype($prediction)
));
}
if (null === $this->promise && !$this->voidReturnType) {
$this->willReturn();
}
$calls = $this->getObjectProphecy()->findProphecyMethodCalls(
$this->getMethodName(),
$this->getArgumentsWildcard()
);
try {
$prediction->check($calls, $this->getObjectProphecy(), $this);
$this->checkedPredictions[] = $prediction;
} catch (\Exception $e) {
$this->checkedPredictions[] = $prediction;
throw $e;
}
return $this;
}
/**
* Checks call prediction.
*
* @see \Prophecy\Prediction\CallPrediction
*
* @return $this
*/
public function shouldHaveBeenCalled()
{
return $this->shouldHave(new Prediction\CallPrediction);
}
/**
* Checks no calls prediction.
*
* @see \Prophecy\Prediction\NoCallsPrediction
*
* @return $this
*/
public function shouldNotHaveBeenCalled()
{
return $this->shouldHave(new Prediction\NoCallsPrediction);
}
/**
* Checks no calls prediction.
*
* @see \Prophecy\Prediction\NoCallsPrediction
* @deprecated
*
* @return $this
*/
public function shouldNotBeenCalled()
{
return $this->shouldNotHaveBeenCalled();
}
/**
* Checks call times prediction.
*
* @see \Prophecy\Prediction\CallTimesPrediction
*
* @param int $count
*
* @return $this
*/
public function shouldHaveBeenCalledTimes($count)
{
return $this->shouldHave(new Prediction\CallTimesPrediction($count));
}
/**
* Checks call times prediction.
*
* @see \Prophecy\Prediction\CallTimesPrediction
*
* @return $this
*/
public function shouldHaveBeenCalledOnce()
{
return $this->shouldHaveBeenCalledTimes(1);
}
/**
* Checks currently registered [with should(...)] prediction.
*/
public function checkPrediction()
{
if (null === $this->prediction) {
return;
}
$this->shouldHave($this->prediction);
}
/**
* Returns currently registered promise.
*
* @return null|Promise\PromiseInterface
*/
public function getPromise()
{
return $this->promise;
}
/**
* Returns currently registered prediction.
*
* @return null|Prediction\PredictionInterface
*/
public function getPrediction()
{
return $this->prediction;
}
/**
* Returns predictions that were checked on this object.
*
* @return Prediction\PredictionInterface[]
*/
public function getCheckedPredictions()
{
return $this->checkedPredictions;
}
/**
* Returns object prophecy this method prophecy is tied to.
*
* @return ObjectProphecy
*/
public function getObjectProphecy()
{
return $this->objectProphecy;
}
/**
* Returns method name.
*
* @return string
*/
public function getMethodName()
{
return $this->methodName;
}
/**
* Returns arguments wildcard.
*
* @return Argument\ArgumentsWildcard
*/
public function getArgumentsWildcard()
{
return $this->argumentsWildcard;
}
/**
* @return bool
*/
public function hasReturnVoid()
{
return $this->voidReturnType;
}
private function bindToObjectProphecy()
{
if ($this->bound) {
return;
}
$this->getObjectProphecy()->addMethodProphecy($this);
$this->bound = true;
}
}

View File

@@ -0,0 +1,281 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Prophecy;
use SebastianBergmann\Comparator\ComparisonFailure;
use Prophecy\Comparator\Factory as ComparatorFactory;
use Prophecy\Call\Call;
use Prophecy\Doubler\LazyDouble;
use Prophecy\Argument\ArgumentsWildcard;
use Prophecy\Call\CallCenter;
use Prophecy\Exception\Prophecy\ObjectProphecyException;
use Prophecy\Exception\Prophecy\MethodProphecyException;
use Prophecy\Exception\Prediction\AggregateException;
use Prophecy\Exception\Prediction\PredictionException;
/**
* Object prophecy.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ObjectProphecy implements ProphecyInterface
{
private $lazyDouble;
private $callCenter;
private $revealer;
private $comparatorFactory;
/**
* @var MethodProphecy[][]
*/
private $methodProphecies = array();
/**
* Initializes object prophecy.
*
* @param LazyDouble $lazyDouble
* @param CallCenter $callCenter
* @param RevealerInterface $revealer
* @param ComparatorFactory $comparatorFactory
*/
public function __construct(
LazyDouble $lazyDouble,
CallCenter $callCenter = null,
RevealerInterface $revealer = null,
ComparatorFactory $comparatorFactory = null
) {
$this->lazyDouble = $lazyDouble;
$this->callCenter = $callCenter ?: new CallCenter;
$this->revealer = $revealer ?: new Revealer;
$this->comparatorFactory = $comparatorFactory ?: ComparatorFactory::getInstance();
}
/**
* Forces double to extend specific class.
*
* @param string $class
*
* @return $this
*/
public function willExtend($class)
{
$this->lazyDouble->setParentClass($class);
return $this;
}
/**
* Forces double to implement specific interface.
*
* @param string $interface
*
* @return $this
*/
public function willImplement($interface)
{
$this->lazyDouble->addInterface($interface);
return $this;
}
/**
* Sets constructor arguments.
*
* @param array $arguments
*
* @return $this
*/
public function willBeConstructedWith(array $arguments = null)
{
$this->lazyDouble->setArguments($arguments);
return $this;
}
/**
* Reveals double.
*
* @return object
*
* @throws \Prophecy\Exception\Prophecy\ObjectProphecyException If double doesn't implement needed interface
*/
public function reveal()
{
$double = $this->lazyDouble->getInstance();
if (null === $double || !$double instanceof ProphecySubjectInterface) {
throw new ObjectProphecyException(
"Generated double must implement ProphecySubjectInterface, but it does not.\n".
'It seems you have wrongly configured doubler without required ClassPatch.',
$this
);
}
$double->setProphecy($this);
return $double;
}
/**
* Adds method prophecy to object prophecy.
*
* @param MethodProphecy $methodProphecy
*
* @throws \Prophecy\Exception\Prophecy\MethodProphecyException If method prophecy doesn't
* have arguments wildcard
*/
public function addMethodProphecy(MethodProphecy $methodProphecy)
{
$argumentsWildcard = $methodProphecy->getArgumentsWildcard();
if (null === $argumentsWildcard) {
throw new MethodProphecyException(sprintf(
"Can not add prophecy for a method `%s::%s()`\n".
"as you did not specify arguments wildcard for it.",
get_class($this->reveal()),
$methodProphecy->getMethodName()
), $methodProphecy);
}
$methodName = $methodProphecy->getMethodName();
if (!isset($this->methodProphecies[$methodName])) {
$this->methodProphecies[$methodName] = array();
}
$this->methodProphecies[$methodName][] = $methodProphecy;
}
/**
* Returns either all or related to single method prophecies.
*
* @param null|string $methodName
*
* @return MethodProphecy[]
*/
public function getMethodProphecies($methodName = null)
{
if (null === $methodName) {
return $this->methodProphecies;
}
if (!isset($this->methodProphecies[$methodName])) {
return array();
}
return $this->methodProphecies[$methodName];
}
/**
* Makes specific method call.
*
* @param string $methodName
* @param array $arguments
*
* @return mixed
*/
public function makeProphecyMethodCall($methodName, array $arguments)
{
$arguments = $this->revealer->reveal($arguments);
$return = $this->callCenter->makeCall($this, $methodName, $arguments);
return $this->revealer->reveal($return);
}
/**
* Finds calls by method name & arguments wildcard.
*
* @param string $methodName
* @param ArgumentsWildcard $wildcard
*
* @return Call[]
*/
public function findProphecyMethodCalls($methodName, ArgumentsWildcard $wildcard)
{
return $this->callCenter->findCalls($methodName, $wildcard);
}
/**
* Checks that registered method predictions do not fail.
*
* @throws \Prophecy\Exception\Prediction\AggregateException If any of registered predictions fail
*/
public function checkProphecyMethodsPredictions()
{
$exception = new AggregateException(sprintf("%s:\n", get_class($this->reveal())));
$exception->setObjectProphecy($this);
foreach ($this->methodProphecies as $prophecies) {
foreach ($prophecies as $prophecy) {
try {
$prophecy->checkPrediction();
} catch (PredictionException $e) {
$exception->append($e);
}
}
}
if (count($exception->getExceptions())) {
throw $exception;
}
}
/**
* Creates new method prophecy using specified method name and arguments.
*
* @param string $methodName
* @param array $arguments
*
* @return MethodProphecy
*/
public function __call($methodName, array $arguments)
{
$arguments = new ArgumentsWildcard($this->revealer->reveal($arguments));
foreach ($this->getMethodProphecies($methodName) as $prophecy) {
$argumentsWildcard = $prophecy->getArgumentsWildcard();
$comparator = $this->comparatorFactory->getComparatorFor(
$argumentsWildcard, $arguments
);
try {
$comparator->assertEquals($argumentsWildcard, $arguments);
return $prophecy;
} catch (ComparisonFailure $failure) {}
}
return new MethodProphecy($this, $methodName, $arguments);
}
/**
* Tries to get property value from double.
*
* @param string $name
*
* @return mixed
*/
public function __get($name)
{
return $this->reveal()->$name;
}
/**
* Tries to set property value to double.
*
* @param string $name
* @param mixed $value
*/
public function __set($name, $value)
{
$this->reveal()->$name = $this->revealer->reveal($value);
}
}

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