├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── ci-push-pr.yml │ ├── ci-scheduled.yml │ ├── ci.yml │ ├── documentation.yml │ └── publish-release.yml ├── .gitignore ├── .php-cs-fixer.php ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MIGRATING.md ├── Makefile ├── README.md ├── composer.json ├── phpstan.neon ├── phpunit.xml └── src ├── Assertion ├── AssertionRecorder.php ├── AssertionRenderer.php ├── Exception │ └── AssertionException.php └── ExceptionAssertionRecorder.php ├── Call ├── Arguments.php ├── Call.php ├── CallData.php ├── CallFactory.php ├── CallVerifier.php ├── CallVerifierFactory.php ├── Event │ ├── CallEvent.php │ ├── CallEventFactory.php │ ├── CallEventTrait.php │ ├── CalledEvent.php │ ├── ConsumedEvent.php │ ├── EndEvent.php │ ├── IterableEvent.php │ ├── ProducedEvent.php │ ├── ReceivedEvent.php │ ├── ReceivedExceptionEvent.php │ ├── ResponseEvent.php │ ├── ReturnedEvent.php │ ├── ThrewEvent.php │ └── UsedEvent.php └── Exception │ ├── UndefinedArgumentException.php │ ├── UndefinedCallException.php │ └── UndefinedResponseException.php ├── Clock ├── Clock.php └── SystemClock.php ├── Collection └── NormalizesIndices.php ├── Difference ├── DifferenceEngine.php └── DifferenceSequenceMatcher.php ├── Event ├── Event.php ├── EventCollection.php ├── EventOrderVerifier.php ├── EventSequence.php └── Exception │ └── UndefinedEventException.php ├── Exporter ├── Exporter.php ├── ExporterResult.php └── InlineExporter.php ├── Facade ├── FacadeContainer.php ├── FacadeContainerTrait.php ├── FacadeTrait.php └── Globals.php ├── Hamcrest ├── HamcrestMatcher.php └── HamcrestMatcherDriver.php ├── Hook ├── Exception │ ├── FunctionExistsException.php │ ├── FunctionHookException.php │ ├── FunctionHookGenerationFailedException.php │ └── FunctionSignatureMismatchException.php ├── FunctionHookGenerator.php └── FunctionHookManager.php ├── Invocation ├── Invocable.php ├── InvocableInspector.php ├── Invoker.php ├── WrappedInvocable.php └── WrappedInvocableTrait.php ├── Matcher ├── AnyMatcher.php ├── EqualToMatcher.php ├── Exception │ └── UndefinedTypeException.php ├── InstanceOfMatcher.php ├── Matcher.php ├── MatcherDriver.php ├── MatcherFactory.php ├── MatcherResult.php ├── MatcherVerifier.php └── WildcardMatcher.php ├── Mock ├── Builder │ ├── Method │ │ ├── CustomMethodDefinition.php │ │ ├── MethodDefinition.php │ │ ├── MethodDefinitionCollection.php │ │ ├── RealMethodDefinition.php │ │ └── TraitMethodDefinition.php │ ├── MockBuilder.php │ ├── MockBuilderFactory.php │ └── MockDefinition.php ├── Exception │ ├── AnonymousClassException.php │ ├── ClassExistsException.php │ ├── FinalClassException.php │ ├── FinalMethodStubException.php │ ├── FinalizedMockException.php │ ├── InvalidClassNameException.php │ ├── InvalidDefinitionException.php │ ├── InvalidMockClassException.php │ ├── InvalidMockException.php │ ├── InvalidTypeException.php │ ├── MockException.php │ ├── MockGenerationFailedException.php │ ├── MultipleInheritanceException.php │ ├── NonMockClassException.php │ └── UndefinedMethodStubException.php ├── Handle │ ├── Handle.php │ ├── HandleFactory.php │ ├── HandleTrait.php │ ├── InstanceHandle.php │ ├── StaticHandle.php │ └── StaticHandleRegistry.php ├── Method │ ├── WrappedCustomMethod.php │ ├── WrappedMagicMethod.php │ ├── WrappedMethod.php │ ├── WrappedMethodTrait.php │ ├── WrappedParentMethod.php │ ├── WrappedTraitMethod.php │ └── WrappedUncallableMethod.php ├── Mock.php ├── MockFactory.php ├── MockGenerator.php └── MockRegistry.php ├── Phony.php ├── Reflection ├── Exception │ └── UndefinedFeatureException.php ├── FeatureDetector.php └── FunctionSignatureInspector.php ├── Sequencer └── Sequencer.php ├── Spy ├── ArraySpy.php ├── Exception │ ├── NonArrayAccessTraversableException.php │ └── NonCountableTraversableException.php ├── GeneratorSpyFactory.php ├── GeneratorSpyMap.php ├── IterableSpy.php ├── IterableSpyFactory.php ├── Spy.php ├── SpyData.php ├── SpyFactory.php ├── SpyVerifier.php ├── SpyVerifierFactory.php └── TraversableSpy.php ├── Stub ├── Answer │ ├── Answer.php │ ├── Builder │ │ ├── GeneratorAnswerBuilder.php │ │ ├── GeneratorAnswerBuilderFactory.php │ │ ├── GeneratorYieldFromIteration.php │ │ └── GeneratorYieldIteration.php │ └── CallRequest.php ├── EmptyValueFactory.php ├── Exception │ ├── FinalReturnTypeException.php │ ├── UndefinedAnswerException.php │ └── UnusedStubCriteriaException.php ├── Stub.php ├── StubData.php ├── StubFactory.php ├── StubRule.php ├── StubVerifier.php └── StubVerifierFactory.php ├── Verification ├── Cardinality.php ├── CardinalityVerifier.php ├── CardinalityVerifierTrait.php ├── Exception │ ├── InvalidCardinalityException.php │ ├── InvalidCardinalityStateException.php │ └── InvalidSingularCardinalityException.php ├── GeneratorVerifier.php ├── GeneratorVerifierFactory.php ├── IterableVerifier.php └── IterableVerifierFactory.php ├── functions.php └── initialize.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /assets/ export-ignore 2 | /doc/ export-ignore 3 | /scripts/ export-ignore 4 | /test/ export-ignore 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | schedule: 5 | interval: weekly 6 | reviewers: 7 | - eloquent/dependabot-reviewers 8 | - package-ecosystem: github-actions 9 | schedule: 10 | interval: weekly 11 | reviewers: 12 | - eloquent/dependabot-reviewers 13 | -------------------------------------------------------------------------------- /.github/workflows/ci-push-pr.yml: -------------------------------------------------------------------------------- 1 | name: CI (push / PR) 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | CI: 7 | name: CI 8 | uses: ./.github/workflows/ci.yml 9 | secrets: inherit 10 | with: 11 | publish-coverage: true 12 | -------------------------------------------------------------------------------- /.github/workflows/ci-scheduled.yml: -------------------------------------------------------------------------------- 1 | name: CI (scheduled) 2 | on: 3 | schedule: 4 | - cron: 0 14 * * 0 # Sunday 2PM UTC = Monday 12AM AEST 5 | jobs: 6 | CI: 7 | name: CI 8 | uses: ./.github/workflows/ci.yml 9 | secrets: inherit 10 | with: 11 | publish-coverage: false 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | workflow_call: 4 | inputs: 5 | publish-coverage: 6 | type: boolean 7 | required: true 8 | jobs: 9 | CI: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | php: ["8.0", "8.1", "8.2"] 15 | name: PHP ${{ matrix.php }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v1 19 | - name: Set up PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | extensions: mbstring 24 | coverage: pcov 25 | - name: Install dependencies 26 | run: make vendor 27 | - name: Make 28 | run: make ci 29 | - name: Publish coverage reports 30 | if: inputs.publish-coverage && success() 31 | uses: codecov/codecov-action@v3 32 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - "[0-9]+.[0-9]+.[0-9]+" 8 | jobs: 9 | documentation: 10 | runs-on: ubuntu-latest 11 | name: Publish documentation 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v1 15 | - name: Set up PHP 16 | uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: "8.2" 19 | extensions: mbstring 20 | coverage: none 21 | - name: Install dependencies 22 | run: make vendor 23 | - name: Publish documentation 24 | if: success() 25 | run: make doc-publish 26 | env: 27 | DOC_GITHUB_TOKEN: ${{ secrets.DOC_GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | name: Publish release 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - name: Publish release 14 | uses: eloquent/github-release-action@v3 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.makefiles/ 2 | /artifacts/ 3 | /composer.lock 4 | /vendor/ 5 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setCacheFile(__DIR__ . '/artifacts/lint/php-cs-fixer/cache'); 5 | $config->setRules(array_merge($config->getRules(), [ 6 | 'phpdoc_to_comment' => false, 7 | 'no_blank_lines_after_phpdoc' => false, 8 | ])); 9 | 10 | $exclusions = [ 11 | 'artifacts', 12 | 'test/fixture', 13 | ]; 14 | 15 | if (version_compare(PHP_VERSION, '8.1.x', '<')) { 16 | $exclusions[] = 'test/src/Test/Enum'; 17 | $exclusions[] = 'test/src/Test/Php81'; 18 | } 19 | 20 | if (version_compare(PHP_VERSION, '8.2.x', '<')) { 21 | $exclusions[] = 'test/src/Test/Php82'; 22 | } 23 | 24 | $config->getFinder()->exclude($exclusions); 25 | 26 | return $config; 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | As a guideline, please follow this process when contributing: 4 | 5 | 1. [Fork the repository] 6 | 2. [Create a branch] 7 | 3. Make your changes 8 | 4. Use `make prepare` to run tests and code style checks 9 | 5. [Squash commits] if necessary 10 | 6. [Create a pull request] 11 | 12 | [create a branch]: https://help.github.com/articles/about-branches 13 | [create a pull request]: https://help.github.com/articles/creating-a-pull-request 14 | [fork the repository]: https://help.github.com/articles/fork-a-repo 15 | [squash commits]: https://help.github.com/articles/about-git-rebase 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2021 Erin Millard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Powered by https://makefiles.dev/ 2 | 3 | export PHP_CS_FIXER_IGNORE_ENV=true 4 | 5 | ################################################################################ 6 | 7 | _HOOK_FIXTURE_INPUT_FILES := $(shell find test/fixture/hook-generator -name callback.php) 8 | _HOOK_FIXTURE_OUTPUT_FILES := $(_HOOK_FIXTURE_INPUT_FILES:callback.php=expected.php) 9 | 10 | _MOCK_FIXTURE_INPUT_FILES := $(shell find test/fixture/mock-generator -name builder.php) 11 | _MOCK_FIXTURE_OUTPUT_FILES := $(_MOCK_FIXTURE_INPUT_FILES:builder.php=expected.php) 12 | 13 | _VERIFICATION_FIXTURE_INPUT_FILES := $(shell find test/fixture/verification -name verification.php) 14 | _VERIFICATION_FIXTURE_OUTPUT_FILES := $(_VERIFICATION_FIXTURE_INPUT_FILES:verification.php=expected) 15 | _VERIFICATION_IMAGE_FILES := $(_VERIFICATION_FIXTURE_INPUT_FILES:test/fixture/verification/%/verification.php=artifacts/build/doc-img/%.svg) 16 | 17 | _DOC_MARKDOWN_FILES := $(wildcard doc/*.md) 18 | _DOC_HTML_FILES := $(_DOC_MARKDOWN_FILES:doc/%.md=artifacts/build/doc-html/%.html) 19 | 20 | GENERATED_FILES += $(_HOOK_FIXTURE_OUTPUT_FILES) $(_MOCK_FIXTURE_OUTPUT_FILES) $(_VERIFICATION_FIXTURE_OUTPUT_FILES) 21 | 22 | ################################################################################ 23 | 24 | -include .makefiles/Makefile 25 | -include .makefiles/pkg/php/v1/Makefile 26 | 27 | .makefiles/%: 28 | @curl -sfL https://makefiles.dev/v1 | bash /dev/stdin "$@" 29 | 30 | ################################################################################ 31 | 32 | .PHONY: doc 33 | doc: artifacts/build/gh-pages 34 | 35 | .PHONY: doc-open 36 | doc-open: 37 | open http://localhost:8080/ 38 | 39 | .PHONY: doc-publish 40 | doc-publish: artifacts/build/gh-pages 41 | scripts/publish-doc "$<" 42 | 43 | .PHONY: doc-serve 44 | doc-serve: artifacts/build/gh-pages 45 | php -S 0.0.0.0:8080 -t "$<" assets/router.php 46 | 47 | .PHONY: output-examples 48 | output-examples: vendor 49 | scripts/output-examples 50 | 51 | .PHONY: test-edge-cases 52 | test-edge-cases: artifacts/test/edge-cases.touch 53 | 54 | .PHONY: test-integration 55 | test-integration: artifacts/test/integration.touch 56 | 57 | ################################################################################ 58 | 59 | artifacts/build/doc-html/%.html: doc/%.md vendor $(wildcard assets/web/*.tpl.html) 60 | @mkdir -p "$(@D)" 61 | scripts/gfm-to-html "$<" "$@" 62 | 63 | artifacts/build/doc-img/%.svg: test/fixture/verification/%/verification.php $(wildcard assets/svg/*.tpl.svg) vendor $(PHP_SOURCE_FILES) 64 | @mkdir -p "$(@D)" 65 | scripts/build-doc-img "$<" "$@" 66 | 67 | artifacts/build/doc: $(wildcard assets/web/css/* assets/web/data/* assets/web/img/* assets/web/js/*) $(_DOC_HTML_FILES) $(_VERIFICATION_IMAGE_FILES) 68 | @rm -rf "$@" 69 | @mkdir -p "$@/img/verification" 70 | @cp -av assets/web/css assets/web/data assets/web/img assets/web/js artifacts/build/doc-html/*.html "$@/" 71 | @cp -av artifacts/build/doc-img/* "$@/img/verification/" 72 | 73 | artifacts/build/gh-pages: artifacts/build/doc artifacts/build/gh-pages-clone vendor $(wildcard assets/web/*.tpl.html) 74 | scripts/refresh-git-clone artifacts/build/gh-pages-clone 75 | @rm -rf "$@" 76 | cp -a artifacts/build/gh-pages-clone "$@" 77 | scripts/update-gh-pages "$<" "$@" 78 | 79 | artifacts/build/gh-pages-clone: 80 | git clone -b gh-pages --single-branch --depth 1 https://github.com/eloquent/phony.git "$@" 81 | 82 | artifacts/test/edge-cases.touch: $(PHP_PHPUNIT_REQ) $(_PHP_PHPUNIT_REQ) 83 | php $(_PHP_PHPUNIT_RUNTIME_ARGS) vendor/bin/phpunit $(_PHP_PHPUNIT_ARGS) --no-coverage test/suite-edge-cases 84 | 85 | @mkdir -p "$(@D)" 86 | @touch "$@" 87 | 88 | artifacts/test/integration.touch: vendor $(PHP_SOURCE_FILES) $(_PHP_TEST_ASSETS) 89 | test/integration/run-all 90 | 91 | @mkdir -p "$(@D)" 92 | @touch "$@" 93 | 94 | test/fixture/hook-generator/%/expected.php: | test/fixture/hook-generator/%/callback.php 95 | scripts/build-hook-generator-fixture "$|" "$@" 96 | 97 | test/fixture/mock-generator/%/expected.php: | test/fixture/mock-generator/%/builder.php 98 | scripts/build-mock-generator-fixture "$|" "$@" 99 | 100 | test/fixture/verification/%/expected: | test/fixture/verification/%/verification.php 101 | scripts/build-verification-fixture "$|" "$@" 102 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eloquent/phony", 3 | "description": "Mocks, stubs, and spies for PHP.", 4 | "keywords": ["mock", "mocking", "stub", "stubbing", "spy", "dummy", "double", "test", "fake"], 5 | "homepage": "https://eloquent-software.com/phony/", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Erin Millard", 10 | "email": "ezzatron@gmail.com", 11 | "homepage": "https://ezzatron.com/" 12 | } 13 | ], 14 | "config": { 15 | "allow-plugins": { 16 | "phpstan/extension-installer": true 17 | } 18 | }, 19 | "require": { 20 | "php": "^8" 21 | }, 22 | "require-dev": { 23 | "ext-pdo": "*", 24 | "eloquent/code-style": "^2", 25 | "eloquent/phpstan-phony": "^0.8", 26 | "friendsofphp/php-cs-fixer": "^3", 27 | "hamcrest/hamcrest-php": "^2", 28 | "phpstan/extension-installer": "^1", 29 | "phpstan/phpstan": "^1", 30 | "phpstan/phpstan-phpunit": "^1", 31 | "phpunit/phpunit": "^9" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Eloquent\\Phony\\": "src" 36 | }, 37 | "files": [ 38 | "src/initialize.php", 39 | "src/functions.php" 40 | ] 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Eloquent\\Phony\\": ["test/src"] 45 | }, 46 | "files": [ 47 | "test/src/ClassA.php", 48 | "test/src/ClassWithProperty.php", 49 | "test/src/initialize.php", 50 | "test/src/Test/functions.php", 51 | "test/src/TestClass.php" 52 | ] 53 | }, 54 | "extra": { 55 | "branch-alias": { 56 | "dev-main": "5.1.x-dev" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src 5 | ignoreErrors: 6 | # allow @throws with interfaces 7 | - message: "/@throws with type .* is not subtype of Throwable/" 8 | paths: 9 | - src/Call/CallData.php 10 | - src/Call/CallVerifier.php 11 | - src/Facade/FacadeTrait.php 12 | - src/functions.php 13 | - src/Hook/FunctionHookManager.php 14 | - src/Invocation/WrappedInvocableTrait.php 15 | - src/Mock/Builder/MockBuilder.php 16 | - src/Mock/Handle/Handle.php 17 | - src/Mock/Handle/HandleFactory.php 18 | - src/Mock/Handle/HandleTrait.php 19 | - src/Mock/MockFactory.php 20 | - src/Verification/Cardinality.php 21 | - src/Verification/CardinalityVerifier.php 22 | - src/Verification/CardinalityVerifierTrait.php 23 | # allow testing for class existence with ReflectionClass constructor 24 | - message: "/class ReflectionClass constructor expects class-string.* string given/" 25 | paths: 26 | - src/Mock/Builder/MockBuilder.php 27 | - src/Mock/Handle/HandleFactory.php 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | test/suite 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | src 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Assertion/AssertionRecorder.php: -------------------------------------------------------------------------------- 1 | $events The events. 30 | * 31 | * @return EventCollection The result. 32 | */ 33 | public function createSuccess(array $events = []): EventCollection; 34 | 35 | /** 36 | * Record that a successful assertion occurred. 37 | * 38 | * @param EventCollection $events The events. 39 | * 40 | * @return EventCollection The result. 41 | */ 42 | public function createSuccessFromEventCollection( 43 | EventCollection $events 44 | ): EventCollection; 45 | 46 | /** 47 | * Create a new assertion failure exception. 48 | * 49 | * @param string $description The failure description. 50 | * 51 | * @return null If this recorder does not throw exceptions. 52 | * @throws Throwable If this recorder throws exceptions. 53 | */ 54 | public function createFailure(string $description); 55 | } 56 | -------------------------------------------------------------------------------- /src/Assertion/Exception/AssertionException.php: -------------------------------------------------------------------------------- 1 | getProperty('trace'); 28 | $traceProperty->setAccessible(true); 29 | /** @var array $trace */ 30 | $trace = $traceProperty->getValue($exception); 31 | $fileProperty = $reflector->getProperty('file'); 32 | $fileProperty->setAccessible(true); 33 | $lineProperty = $reflector->getProperty('line'); 34 | $lineProperty->setAccessible(true); 35 | 36 | $call = static::tracePhonyCall($trace); 37 | 38 | if (empty($call)) { 39 | $traceProperty->setValue($exception, []); 40 | $fileProperty->setValue($exception, ''); 41 | $lineProperty->setValue($exception, 0); 42 | } else { 43 | $traceProperty->setValue($exception, [$call]); 44 | $fileProperty->setValue($exception, $call['file'] ?? ''); 45 | $lineProperty->setValue($exception, $call['line'] ?? 0); 46 | } 47 | } 48 | 49 | /** 50 | * Find the Phony entry point call in a stack trace. 51 | * 52 | * @param array $trace The stack trace. 53 | * 54 | * @return array The call, or an empty array if unable to determine the entry point. 55 | */ 56 | public static function tracePhonyCall(array $trace): array 57 | { 58 | $prefix = 'Eloquent\Phony\\'; 59 | 60 | for ($i = count($trace) - 1; $i >= 0; --$i) { 61 | $entry = $trace[$i]; 62 | 63 | if (isset($entry['class'])) { 64 | if (0 === strpos($entry['class'], $prefix)) { 65 | return $entry; 66 | } 67 | } elseif (0 === strpos($entry['function'], $prefix)) { 68 | return $entry; 69 | } 70 | } 71 | 72 | return []; 73 | } 74 | 75 | /** 76 | * Construct a new assertion exception. 77 | * 78 | * @param string $description The failure description. 79 | */ 80 | public function __construct(string $description) 81 | { 82 | parent::__construct($description); 83 | 84 | static::trim($this); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Assertion/ExceptionAssertionRecorder.php: -------------------------------------------------------------------------------- 1 | callVerifierFactory = $callVerifierFactory; 27 | } 28 | 29 | /** 30 | * Record that a successful assertion occurred. 31 | * 32 | * @param array $events The events. 33 | * 34 | * @return EventCollection The result. 35 | */ 36 | public function createSuccess(array $events = []): EventCollection 37 | { 38 | return new EventSequence($events, $this->callVerifierFactory); 39 | } 40 | 41 | /** 42 | * Record that a successful assertion occurred. 43 | * 44 | * @param EventCollection $events The events. 45 | * 46 | * @return EventCollection The result. 47 | */ 48 | public function createSuccessFromEventCollection( 49 | EventCollection $events 50 | ): EventCollection { 51 | return $events; 52 | } 53 | 54 | /** 55 | * Create a new assertion failure exception. 56 | * 57 | * @param string $description The failure description. 58 | * 59 | * @return null This method never returns. 60 | * @throws AssertionException The assertion failure exception. 61 | */ 62 | public function createFailure(string $description) 63 | { 64 | throw new AssertionException($description); 65 | } 66 | 67 | /** 68 | * @var CallVerifierFactory 69 | */ 70 | private $callVerifierFactory; 71 | } 72 | -------------------------------------------------------------------------------- /src/Call/CallFactory.php: -------------------------------------------------------------------------------- 1 | eventFactory = $eventFactory; 28 | $this->invoker = $invoker; 29 | } 30 | 31 | /** 32 | * Record call details by invoking a callback. 33 | * 34 | * @param callable $callback The callback. 35 | * @param Arguments $arguments The arguments. 36 | * @param SpyData $spy The spy to record the call to. 37 | * 38 | * @return CallData The newly created call. 39 | */ 40 | public function record( 41 | callable $callback, 42 | Arguments $arguments, 43 | SpyData $spy 44 | ): CallData { 45 | $originalArguments = $arguments->copy(); 46 | 47 | $call = new CallData( 48 | $spy->nextIndex(), 49 | $this->eventFactory->createCalled($spy, $originalArguments) 50 | ); 51 | $spy->addCall($call); 52 | 53 | $returnValue = null; 54 | $exception = null; 55 | 56 | try { 57 | $returnValue = $this->invoker->callWith($callback, $arguments); 58 | } catch (Throwable $exception) { 59 | // handled below 60 | } 61 | 62 | if ($exception) { 63 | $responseEvent = $this->eventFactory->createThrew($exception); 64 | } else { 65 | $responseEvent = $this->eventFactory->createReturned($returnValue); 66 | } 67 | 68 | $call->setResponseEvent($responseEvent); 69 | 70 | return $call; 71 | } 72 | 73 | /** 74 | * @var CallEventFactory 75 | */ 76 | private $eventFactory; 77 | 78 | /** 79 | * @var Invoker 80 | */ 81 | private $invoker; 82 | } 83 | -------------------------------------------------------------------------------- /src/Call/CallVerifierFactory.php: -------------------------------------------------------------------------------- 1 | matcherFactory = $matcherFactory; 38 | $this->matcherVerifier = $matcherVerifier; 39 | $this->generatorVerifierFactory = $generatorVerifierFactory; 40 | $this->iterableVerifierFactory = $iterableVerifierFactory; 41 | $this->assertionRecorder = $assertionRecorder; 42 | $this->assertionRenderer = $assertionRenderer; 43 | } 44 | 45 | /** 46 | * Wrap the supplied call in a verifier. 47 | * 48 | * @param Call $call The call. 49 | * 50 | * @return CallVerifier The call verifier. 51 | */ 52 | public function fromCall(Call $call): CallVerifier 53 | { 54 | return new CallVerifier( 55 | $call, 56 | $this->matcherFactory, 57 | $this->matcherVerifier, 58 | $this->generatorVerifierFactory, 59 | $this->iterableVerifierFactory, 60 | $this->assertionRecorder, 61 | $this->assertionRenderer 62 | ); 63 | } 64 | 65 | /** 66 | * Wrap the supplied calls in verifiers. 67 | * 68 | * @param array $calls The calls. 69 | * 70 | * @return array The call verifiers. 71 | */ 72 | public function fromCalls(array $calls): array 73 | { 74 | $verifiers = []; 75 | 76 | foreach ($calls as $call) { 77 | $verifiers[] = new CallVerifier( 78 | $call, 79 | $this->matcherFactory, 80 | $this->matcherVerifier, 81 | $this->generatorVerifierFactory, 82 | $this->iterableVerifierFactory, 83 | $this->assertionRecorder, 84 | $this->assertionRenderer 85 | ); 86 | } 87 | 88 | return $verifiers; 89 | } 90 | 91 | /** 92 | * @var MatcherFactory 93 | */ 94 | private $matcherFactory; 95 | 96 | /** 97 | * @var MatcherVerifier 98 | */ 99 | private $matcherVerifier; 100 | 101 | /** 102 | * @var GeneratorVerifierFactory 103 | */ 104 | private $generatorVerifierFactory; 105 | 106 | /** 107 | * @var IterableVerifierFactory 108 | */ 109 | private $iterableVerifierFactory; 110 | 111 | /** 112 | * @var AssertionRecorder 113 | */ 114 | private $assertionRecorder; 115 | 116 | /** 117 | * @var AssertionRenderer 118 | */ 119 | private $assertionRenderer; 120 | } 121 | -------------------------------------------------------------------------------- /src/Call/Event/CallEvent.php: -------------------------------------------------------------------------------- 1 | sequencer = $sequencer; 26 | $this->clock = $clock; 27 | } 28 | 29 | /** 30 | * Create a new 'called' event. 31 | * 32 | * @param callable $callback The callback. 33 | * @param Arguments $arguments The arguments. 34 | * 35 | * @return CalledEvent The newly created event. 36 | */ 37 | public function createCalled( 38 | callable $callback, 39 | Arguments $arguments 40 | ): CalledEvent { 41 | return new CalledEvent( 42 | $this->sequencer->next(), 43 | $this->clock->time(), 44 | $callback, 45 | $arguments 46 | ); 47 | } 48 | 49 | /** 50 | * Create a new 'returned' event. 51 | * 52 | * @param mixed $value The return value. 53 | * 54 | * @return ReturnedEvent The newly created event. 55 | */ 56 | public function createReturned($value): ReturnedEvent 57 | { 58 | return new ReturnedEvent( 59 | $this->sequencer->next(), 60 | $this->clock->time(), 61 | $value 62 | ); 63 | } 64 | 65 | /** 66 | * Create a new 'thrown' event. 67 | * 68 | * @param Throwable $exception The thrown exception. 69 | * 70 | * @return ThrewEvent The newly created event. 71 | */ 72 | public function createThrew(Throwable $exception): ThrewEvent 73 | { 74 | return new ThrewEvent( 75 | $this->sequencer->next(), 76 | $this->clock->time(), 77 | $exception 78 | ); 79 | } 80 | 81 | /** 82 | * Create a new 'used' event. 83 | * 84 | * @return UsedEvent The newly created event. 85 | */ 86 | public function createUsed(): UsedEvent 87 | { 88 | return new UsedEvent($this->sequencer->next(), $this->clock->time()); 89 | } 90 | 91 | /** 92 | * Create a new 'produced' event. 93 | * 94 | * @param mixed $key The produced key. 95 | * @param mixed $value The produced value. 96 | * 97 | * @return ProducedEvent The newly created event. 98 | */ 99 | public function createProduced($key, $value): ProducedEvent 100 | { 101 | return new ProducedEvent( 102 | $this->sequencer->next(), 103 | $this->clock->time(), 104 | $key, 105 | $value 106 | ); 107 | } 108 | 109 | /** 110 | * Create a new 'received' event. 111 | * 112 | * @param mixed $value The received value. 113 | * 114 | * @return ReceivedEvent The newly created event. 115 | */ 116 | public function createReceived($value): ReceivedEvent 117 | { 118 | return new ReceivedEvent( 119 | $this->sequencer->next(), 120 | $this->clock->time(), 121 | $value 122 | ); 123 | } 124 | 125 | /** 126 | * Create a new 'received exception' event. 127 | * 128 | * @param Throwable $exception The received exception. 129 | * 130 | * @return ReceivedExceptionEvent The newly created event. 131 | */ 132 | public function createReceivedException( 133 | Throwable $exception 134 | ): ReceivedExceptionEvent { 135 | return new ReceivedExceptionEvent( 136 | $this->sequencer->next(), 137 | $this->clock->time(), 138 | $exception 139 | ); 140 | } 141 | 142 | /** 143 | * Create a new 'consumed' event. 144 | * 145 | * @return ConsumedEvent The newly created event. 146 | */ 147 | public function createConsumed(): ConsumedEvent 148 | { 149 | return new ConsumedEvent( 150 | $this->sequencer->next(), 151 | $this->clock->time() 152 | ); 153 | } 154 | 155 | /** 156 | * @var Sequencer 157 | */ 158 | private $sequencer; 159 | 160 | /** 161 | * @var Clock 162 | */ 163 | private $clock; 164 | } 165 | -------------------------------------------------------------------------------- /src/Call/Event/CallEventTrait.php: -------------------------------------------------------------------------------- 1 | sequenceNumber; 26 | } 27 | 28 | /** 29 | * Get the time at which the event occurred. 30 | * 31 | * @return float The time at which the event occurred, in seconds since the Unix epoch. 32 | */ 33 | public function time(): float 34 | { 35 | return $this->time; 36 | } 37 | 38 | /** 39 | * Set the call. 40 | * 41 | * @param Call $call The call. 42 | * 43 | * @return $this This event. 44 | */ 45 | public function setCall(Call $call): CallEvent 46 | { 47 | $this->call = $call; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Get the call. 54 | * 55 | * @return ?Call The call, or null if no call has been set. 56 | */ 57 | public function call(): ?Call 58 | { 59 | return $this->call; 60 | } 61 | 62 | /** 63 | * @var int 64 | */ 65 | private $sequenceNumber; 66 | 67 | /** 68 | * @var float 69 | */ 70 | private $time; 71 | 72 | /** 73 | * @var ?Call 74 | */ 75 | private $call; 76 | } 77 | -------------------------------------------------------------------------------- /src/Call/Event/CalledEvent.php: -------------------------------------------------------------------------------- 1 | sequenceNumber = $sequenceNumber; 31 | $this->time = $time; 32 | $this->callback = $callback; 33 | $this->arguments = $arguments; 34 | } 35 | 36 | /** 37 | * Get the callback. 38 | * 39 | * @return callable The callback. 40 | */ 41 | public function callback(): callable 42 | { 43 | return $this->callback; 44 | } 45 | 46 | /** 47 | * Get the received arguments. 48 | * 49 | * @return Arguments The received arguments. 50 | */ 51 | public function arguments(): Arguments 52 | { 53 | return $this->arguments; 54 | } 55 | 56 | /** 57 | * @var callable 58 | */ 59 | private $callback; 60 | 61 | /** 62 | * @var Arguments 63 | */ 64 | private $arguments; 65 | } 66 | -------------------------------------------------------------------------------- /src/Call/Event/ConsumedEvent.php: -------------------------------------------------------------------------------- 1 | sequenceNumber = $sequenceNumber; 23 | $this->time = $time; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Call/Event/EndEvent.php: -------------------------------------------------------------------------------- 1 | sequenceNumber = $sequenceNumber; 25 | $this->time = $time; 26 | $this->key = $key; 27 | $this->value = $value; 28 | } 29 | 30 | /** 31 | * Get the produced key. 32 | * 33 | * @return mixed The produced key. 34 | */ 35 | public function key() 36 | { 37 | return $this->key; 38 | } 39 | 40 | /** 41 | * Get the produced value. 42 | * 43 | * @return mixed The produced value. 44 | */ 45 | public function value() 46 | { 47 | return $this->value; 48 | } 49 | 50 | /** 51 | * @var mixed 52 | */ 53 | private $key; 54 | 55 | /** 56 | * @var mixed 57 | */ 58 | private $value; 59 | } 60 | -------------------------------------------------------------------------------- /src/Call/Event/ReceivedEvent.php: -------------------------------------------------------------------------------- 1 | sequenceNumber = $sequenceNumber; 24 | $this->time = $time; 25 | $this->value = $value; 26 | } 27 | 28 | /** 29 | * Get the received value. 30 | * 31 | * @return mixed The received value. 32 | */ 33 | public function value() 34 | { 35 | return $this->value; 36 | } 37 | 38 | /** 39 | * @var mixed 40 | */ 41 | private $value; 42 | } 43 | -------------------------------------------------------------------------------- /src/Call/Event/ReceivedExceptionEvent.php: -------------------------------------------------------------------------------- 1 | sequenceNumber = $sequenceNumber; 29 | $this->time = $time; 30 | $this->exception = $exception; 31 | } 32 | 33 | /** 34 | * Get the received exception. 35 | * 36 | * @return Throwable The received exception. 37 | */ 38 | public function exception(): Throwable 39 | { 40 | return $this->exception; 41 | } 42 | 43 | /** 44 | * @var Throwable 45 | */ 46 | private $exception; 47 | } 48 | -------------------------------------------------------------------------------- /src/Call/Event/ResponseEvent.php: -------------------------------------------------------------------------------- 1 | sequenceNumber = $sequenceNumber; 24 | $this->time = $time; 25 | $this->value = $value; 26 | } 27 | 28 | /** 29 | * Get the returned value. 30 | * 31 | * @return mixed The returned value. 32 | */ 33 | public function value() 34 | { 35 | return $this->value; 36 | } 37 | 38 | /** 39 | * @var mixed 40 | */ 41 | private $value; 42 | } 43 | -------------------------------------------------------------------------------- /src/Call/Event/ThrewEvent.php: -------------------------------------------------------------------------------- 1 | sequenceNumber = $sequenceNumber; 29 | $this->time = $time; 30 | $this->exception = $exception; 31 | } 32 | 33 | /** 34 | * Get the thrown exception. 35 | * 36 | * @return Throwable The thrown exception. 37 | */ 38 | public function exception(): Throwable 39 | { 40 | return $this->exception; 41 | } 42 | 43 | /** 44 | * @var Throwable 45 | */ 46 | private $exception; 47 | } 48 | -------------------------------------------------------------------------------- /src/Call/Event/UsedEvent.php: -------------------------------------------------------------------------------- 1 | sequenceNumber = $sequenceNumber; 23 | $this->time = $time; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Call/Exception/UndefinedArgumentException.php: -------------------------------------------------------------------------------- 1 | index = $index; 22 | 23 | parent::__construct( 24 | sprintf( 25 | 'No argument defined for index %s.', 26 | var_export($index, true) 27 | ) 28 | ); 29 | } 30 | 31 | /** 32 | * Get the index. 33 | * 34 | * @return int The index. 35 | */ 36 | public function index(): int 37 | { 38 | return $this->index; 39 | } 40 | 41 | /** 42 | * @var int 43 | */ 44 | private $index; 45 | } 46 | -------------------------------------------------------------------------------- /src/Call/Exception/UndefinedCallException.php: -------------------------------------------------------------------------------- 1 | index = $index; 22 | 23 | parent::__construct( 24 | sprintf('No call defined for index %s.', var_export($index, true)) 25 | ); 26 | } 27 | 28 | /** 29 | * Get the call index. 30 | * 31 | * @return int The call index. 32 | */ 33 | public function index(): int 34 | { 35 | return $this->index; 36 | } 37 | 38 | /** 39 | * @var int 40 | */ 41 | private $index; 42 | } 43 | -------------------------------------------------------------------------------- /src/Call/Exception/UndefinedResponseException.php: -------------------------------------------------------------------------------- 1 | microtime = $microtime; 20 | } 21 | 22 | /** 23 | * Get the current time. 24 | * 25 | * @return float The current time. 26 | */ 27 | public function time(): float 28 | { 29 | $microtime = $this->microtime; 30 | 31 | return $microtime(true); 32 | } 33 | 34 | /** 35 | * @var callable 36 | */ 37 | private $microtime; 38 | } 39 | -------------------------------------------------------------------------------- /src/Collection/NormalizesIndices.php: -------------------------------------------------------------------------------- 1 | = $size) { 30 | return false; 31 | } 32 | 33 | $normalized = $potential; 34 | 35 | return true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Difference/DifferenceEngine.php: -------------------------------------------------------------------------------- 1 | featureDetector = $featureDetector; 22 | 23 | $this->setUseColor(null); 24 | } 25 | 26 | /** 27 | * Turn on or off the use of ANSI colored output. 28 | * 29 | * Pass `null` to detect automatically. 30 | * 31 | * @param ?bool $useColor True to use color. 32 | */ 33 | public function setUseColor(?bool $useColor): void 34 | { 35 | if (null === $useColor) { 36 | $useColor = $this->featureDetector->isSupported('stdout.ansi'); 37 | } 38 | 39 | // @codeCoverageIgnoreStart 40 | if ($useColor) { 41 | $this->addStart = "\033[33m\033[2m{+\033[0m\033[33m\033[4m"; 42 | $this->addEnd = "\033[0m\033[33m\033[2m+}\033[0m"; 43 | $this->removeStart = "\033[36m\033[2m[-\033[0m\033[36m\033[4m"; 44 | $this->removeEnd = "\033[0m\033[36m\033[2m-]\033[0m"; 45 | } else { 46 | // @codeCoverageIgnoreEnd 47 | $this->addStart = '{+'; 48 | $this->addEnd = '+}'; 49 | $this->removeStart = '[-'; 50 | $this->removeEnd = '-]'; 51 | } 52 | } 53 | 54 | /** 55 | * Get the difference between the supplied strings. 56 | * 57 | * @param string $from The from value. 58 | * @param string $to The to value. 59 | * 60 | * @return string The difference. 61 | */ 62 | public function difference(string $from, string $to): string 63 | { 64 | $flags = PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY; 65 | /** @var array */ 66 | $from = preg_split('/(\W+)/u', $from, -1, $flags); 67 | /** @var array */ 68 | $to = preg_split('/(\W+)/u', $to, -1, $flags); 69 | 70 | $matcher = new DifferenceSequenceMatcher($from, $to); 71 | $diff = ''; 72 | 73 | foreach ($matcher->getOpcodes() as $opcode) { 74 | list($tag, $i1, $i2, $j1, $j2) = $opcode; 75 | 76 | if ($tag === 'equal') { 77 | $diff .= implode(array_slice($from, $i1, $i2 - $i1)); 78 | } else { 79 | if ($tag === 'replace' || $tag === 'delete') { 80 | $diff .= 81 | $this->removeStart . 82 | implode(array_slice($from, $i1, $i2 - $i1)) . 83 | $this->removeEnd; 84 | } 85 | 86 | if ($tag === 'replace' || $tag === 'insert') { 87 | $diff .= 88 | $this->addStart . 89 | implode(array_slice($to, $j1, $j2 - $j1)) . 90 | $this->addEnd; 91 | } 92 | } 93 | } 94 | 95 | return $diff; 96 | } 97 | 98 | /** 99 | * @var FeatureDetector 100 | */ 101 | private $featureDetector; 102 | 103 | /** 104 | * @var string 105 | */ 106 | private $addStart; 107 | 108 | /** 109 | * @var string 110 | */ 111 | private $addEnd; 112 | 113 | /** 114 | * @var string 115 | */ 116 | private $removeStart; 117 | 118 | /** 119 | * @var string 120 | */ 121 | private $removeEnd; 122 | } 123 | -------------------------------------------------------------------------------- /src/Event/Event.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | interface EventCollection extends IteratorAggregate, Countable 19 | { 20 | /** 21 | * Returns true if this collection contains any events. 22 | * 23 | * @return bool True if this collection contains any events. 24 | */ 25 | public function hasEvents(): bool; 26 | 27 | /** 28 | * Returns true if this collection contains any calls. 29 | * 30 | * @return bool True if this collection contains any calls. 31 | */ 32 | public function hasCalls(): bool; 33 | 34 | /** 35 | * Get the number of events. 36 | * 37 | * @return int The event count. 38 | */ 39 | public function eventCount(): int; 40 | 41 | /** 42 | * Get the number of calls. 43 | * 44 | * @return int The call count. 45 | */ 46 | public function callCount(): int; 47 | 48 | /** 49 | * Get all events as an array. 50 | * 51 | * @return array The events. 52 | */ 53 | public function allEvents(): array; 54 | 55 | /** 56 | * Get all calls as an array. 57 | * 58 | * @return array The calls. 59 | */ 60 | public function allCalls(): array; 61 | 62 | /** 63 | * Get the first event. 64 | * 65 | * @return Event The event. 66 | * @throws UndefinedEventException If there are no events. 67 | */ 68 | public function firstEvent(): Event; 69 | 70 | /** 71 | * Get the last event. 72 | * 73 | * @return Event The event. 74 | * @throws UndefinedEventException If there are no events. 75 | */ 76 | public function lastEvent(): Event; 77 | 78 | /** 79 | * Get an event by index. 80 | * 81 | * Negative indices are offset from the end of the list. That is, `-1` 82 | * indicates the last element, and `-2` indicates the second last element. 83 | * 84 | * @param int $index The index. 85 | * 86 | * @return Event The event. 87 | * @throws UndefinedEventException If the requested event is undefined, or there are no events. 88 | */ 89 | public function eventAt(int $index = 0): Event; 90 | 91 | /** 92 | * Get the first call. 93 | * 94 | * @return Call The call. 95 | * @throws UndefinedCallException If there are no calls. 96 | */ 97 | public function firstCall(): Call; 98 | 99 | /** 100 | * Get the last call. 101 | * 102 | * @return Call The call. 103 | * @throws UndefinedCallException If there are no calls. 104 | */ 105 | public function lastCall(): Call; 106 | 107 | /** 108 | * Get a call by index. 109 | * 110 | * Negative indices are offset from the end of the list. That is, `-1` 111 | * indicates the last element, and `-2` indicates the second last element. 112 | * 113 | * @param int $index The index. 114 | * 115 | * @return Call The call. 116 | * @throws UndefinedCallException If the requested call is undefined, or there are no calls. 117 | */ 118 | public function callAt(int $index = 0): Call; 119 | } 120 | -------------------------------------------------------------------------------- /src/Event/Exception/UndefinedEventException.php: -------------------------------------------------------------------------------- 1 | index = $index; 24 | 25 | parent::__construct( 26 | sprintf('No event defined for index %d.', $index), 27 | 0, 28 | $cause 29 | ); 30 | } 31 | 32 | /** 33 | * Get the index. 34 | * 35 | * @return int The index. 36 | */ 37 | public function index(): int 38 | { 39 | return $this->index; 40 | } 41 | 42 | /** 43 | * @var int 44 | */ 45 | private $index; 46 | } 47 | -------------------------------------------------------------------------------- /src/Exporter/Exporter.php: -------------------------------------------------------------------------------- 1 | > 72 | */ 73 | public $children = []; 74 | } 75 | -------------------------------------------------------------------------------- /src/Facade/FacadeContainer.php: -------------------------------------------------------------------------------- 1 | initializeContainer(new ExceptionAssertionRecorder()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Facade/Globals.php: -------------------------------------------------------------------------------- 1 | matcher = $matcher; 24 | } 25 | 26 | /** 27 | * Returns `true` if `$value` matches this matcher's criteria. 28 | * 29 | * @param mixed $value The value to check. 30 | * 31 | * @return bool True if the value matches. 32 | */ 33 | public function matches($value): bool 34 | { 35 | return (bool) $this->matcher->matches($value); 36 | } 37 | 38 | /** 39 | * Describe this matcher. 40 | * 41 | * @param ?Exporter $exporter The exporter to use. 42 | * 43 | * @return string The description. 44 | */ 45 | public function describe(Exporter $exporter = null): string 46 | { 47 | return '<' . strval($this->matcher) . '>'; 48 | } 49 | 50 | /** 51 | * Describe this matcher. 52 | * 53 | * @return string The description. 54 | */ 55 | public function __toString(): string 56 | { 57 | return '<' . strval($this->matcher) . '>'; 58 | } 59 | 60 | /** 61 | * @var ExternalMatcher 62 | */ 63 | private $matcher; 64 | } 65 | -------------------------------------------------------------------------------- /src/Hamcrest/HamcrestMatcherDriver.php: -------------------------------------------------------------------------------- 1 | The matcher class names. 30 | */ 31 | public function matcherClassNames(): array 32 | { 33 | return [ExternalMatcher::class]; 34 | } 35 | 36 | /** 37 | * Wrap the supplied third party matcher. 38 | * 39 | * @param ExternalMatcher $matcher The matcher to wrap. 40 | * 41 | * @return Matcher The wrapped matcher. 42 | */ 43 | public function wrapMatcher(object $matcher): Matcher 44 | { 45 | return new HamcrestMatcher($matcher); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Hook/Exception/FunctionExistsException.php: -------------------------------------------------------------------------------- 1 | functionName = $functionName; 23 | 24 | parent::__construct( 25 | sprintf( 26 | 'Function %s is already defined.', 27 | var_export($functionName, true) 28 | ) 29 | ); 30 | } 31 | 32 | /** 33 | * Get the function name. 34 | * 35 | * @return string The function name. 36 | */ 37 | public function functionName(): string 38 | { 39 | return $this->functionName; 40 | } 41 | 42 | /** 43 | * @var string 44 | */ 45 | private $functionName; 46 | } 47 | -------------------------------------------------------------------------------- /src/Hook/Exception/FunctionHookException.php: -------------------------------------------------------------------------------- 1 | functionName = $functionName; 33 | $this->callback = $callback; 34 | $this->source = $source; 35 | $this->error = $error; 36 | 37 | $lines = explode(PHP_EOL, $source); 38 | 39 | if (null === $error) { 40 | $message = sprintf( 41 | 'Function hook %s generation failed.%sRelevant lines:%%s', 42 | $functionName, 43 | PHP_EOL 44 | ); 45 | $errorLineNumber = null; 46 | } else { 47 | /** @var int */ 48 | $errorLineNumber = $error['line']; 49 | $startLine = $errorLineNumber - 4; 50 | $contextLineCount = 7; 51 | 52 | if ($startLine < 0) { 53 | $contextLineCount += $startLine; 54 | $startLine = 0; 55 | } 56 | 57 | $lines = array_slice($lines, $startLine, $contextLineCount, true); 58 | 59 | $message = sprintf( 60 | 'Function hook %s generation failed: ' . 61 | '%s in generated code on line %d.%s' . 62 | 'Relevant lines:%%s', 63 | $functionName, 64 | $error['message'], 65 | $errorLineNumber, 66 | PHP_EOL 67 | ); 68 | } 69 | 70 | end($lines); 71 | $lineNumber = key($lines); 72 | $padSize = strlen((string) ($lineNumber + 1)) + 4; 73 | $renderedLines = ''; 74 | 75 | foreach ($lines as $lineNumber => $line) { 76 | if (null !== $errorLineNumber) { 77 | $highlight = $lineNumber + 1 === $errorLineNumber; 78 | } else { 79 | $highlight = false; 80 | } 81 | 82 | $renderedLines .= sprintf( 83 | '%s%s%s %s', 84 | PHP_EOL, 85 | str_pad( 86 | (string) ($lineNumber + 1), 87 | $padSize, 88 | ' ', 89 | STR_PAD_LEFT 90 | ), 91 | $highlight ? ':' : ' ', 92 | $line 93 | ); 94 | } 95 | 96 | parent::__construct(sprintf($message, $renderedLines), 0, $cause); 97 | } 98 | 99 | /** 100 | * Get the function name. 101 | * 102 | * @return string The function name. 103 | */ 104 | public function functionName(): string 105 | { 106 | return $this->functionName; 107 | } 108 | 109 | /** 110 | * Get the callback. 111 | * 112 | * @return callable The callback. 113 | */ 114 | public function callback(): callable 115 | { 116 | return $this->callback; 117 | } 118 | 119 | /** 120 | * Get the generated source code. 121 | * 122 | * @return string The generated source code. 123 | */ 124 | public function source(): string 125 | { 126 | return $this->source; 127 | } 128 | 129 | /** 130 | * Get the error details. 131 | * 132 | * @return ?array The error details. 133 | */ 134 | public function error(): ?array 135 | { 136 | return $this->error; 137 | } 138 | 139 | /** 140 | * @var string 141 | */ 142 | private $functionName; 143 | 144 | /** 145 | * @var callable 146 | */ 147 | private $callback; 148 | 149 | /** 150 | * @var string 151 | */ 152 | private $source; 153 | 154 | /** 155 | * @var ?array 156 | */ 157 | private $error; 158 | } 159 | -------------------------------------------------------------------------------- /src/Hook/Exception/FunctionSignatureMismatchException.php: -------------------------------------------------------------------------------- 1 | functionName = $functionName; 23 | 24 | parent::__construct( 25 | sprintf( 26 | 'Function %s has a different signature to the supplied ' . 27 | 'callback.', 28 | var_export($functionName, true) 29 | ) 30 | ); 31 | } 32 | 33 | /** 34 | * Get the function name. 35 | * 36 | * @return string The function name. 37 | */ 38 | public function functionName(): string 39 | { 40 | return $this->functionName; 41 | } 42 | 43 | /** 44 | * @var string 45 | */ 46 | private $functionName; 47 | } 48 | -------------------------------------------------------------------------------- /src/Hook/FunctionHookGenerator.php: -------------------------------------------------------------------------------- 1 | >,string} $signature The function signature. 18 | * 19 | * @return string The source code. 20 | */ 21 | public function generateHook( 22 | string $name, 23 | string $namespace, 24 | array $signature 25 | ): string { 26 | $arguments = self::VAR_PREFIX . 'arguments'; 27 | $argumentCount = self::VAR_PREFIX . 'argumentCount'; 28 | $i = self::VAR_PREFIX . 'i'; 29 | $nameVar = self::VAR_PREFIX . 'name'; 30 | $callback = self::VAR_PREFIX . 'callback'; 31 | 32 | $source = "namespace $namespace;\n\nfunction $name"; 33 | list($parameters) = $signature; 34 | $parameterCount = count($parameters); 35 | 36 | if ($parameterCount > 0) { 37 | $isFirst = true; 38 | 39 | foreach ($parameters as $parameterName => $parameter) { 40 | if ($isFirst) { 41 | $isFirst = false; 42 | $source .= "(\n "; 43 | } else { 44 | $source .= ",\n "; 45 | } 46 | 47 | $source .= $parameter[0] . 48 | $parameter[1] . 49 | $parameter[2] . 50 | '$' . $parameterName . 51 | $parameter[3]; 52 | } 53 | 54 | $source .= "\n) {\n"; 55 | } else { 56 | $source .= "()\n{\n"; 57 | } 58 | 59 | $variadicIndex = -1; 60 | $variadicReference = ''; 61 | $variadicName = ''; 62 | 63 | if ($parameterCount > 0) { 64 | $argumentPacking = "\n"; 65 | $index = -1; 66 | 67 | foreach ($parameters as $parameterName => $parameter) { 68 | if ($parameter[2]) { 69 | --$parameterCount; 70 | 71 | $variadicIndex = ++$index; 72 | $variadicReference = $parameter[1]; 73 | $variadicName = $parameterName; 74 | } else { 75 | $argumentPacking .= 76 | "\n if ($argumentCount > " . 77 | ++$index . 78 | ") {\n {$arguments}[] = " . 79 | $parameter[1] . 80 | '$' . $parameterName . 81 | ";\n }"; 82 | } 83 | } 84 | } else { 85 | $argumentPacking = ''; 86 | } 87 | 88 | $source .= 89 | " $argumentCount = \\func_num_args();\n" . 90 | " $arguments = [];" . 91 | $argumentPacking . 92 | "\n\n for ($i = " . 93 | $parameterCount . 94 | "; $i < $argumentCount; ++$i) {\n"; 95 | 96 | if ($variadicIndex > -1) { 97 | $source .= 98 | " {$arguments}[] = $variadicReference\$" . 99 | "{$variadicName}[$i - $variadicIndex];\n" . 100 | ' }'; 101 | } else { 102 | $source .= 103 | " {$arguments}[] = \\func_get_arg($i);\n" . 104 | ' }'; 105 | } 106 | 107 | $ret = 'ret' . 'urn'; 108 | 109 | $renderedName = var_export(strtolower($namespace . '\\' . $name), true); 110 | $source .= 111 | "\n\n $nameVar = $renderedName;\n\n if (" . 112 | "\n !isset(\n " . 113 | '\Eloquent\Phony\Hook\FunctionHookManager::$hooks' . "[$nameVar]" . 114 | "['callback']\n )\n ) {\n " . 115 | "$ret \\$name(...$arguments);" . 116 | "\n }\n\n $callback =\n " . 117 | '\Eloquent\Phony\Hook\FunctionHookManager::$hooks' . 118 | "[$nameVar]['callback'];\n\n" . 119 | " if ($callback instanceof " . 120 | "\Eloquent\Phony\Invocation\Invocable) {\n" . 121 | " $ret {$callback}->invokeWith($arguments);\n" . 122 | " }\n\n " . 123 | "$ret $callback(...$arguments);\n}\n"; 124 | 125 | // @codeCoverageIgnoreStart 126 | if ("\n" !== PHP_EOL) { 127 | $source = str_replace("\n", PHP_EOL, $source); 128 | } 129 | // @codeCoverageIgnoreEnd 130 | 131 | return $source; 132 | } 133 | 134 | const VAR_PREFIX = "$\u{a4}"; 135 | } 136 | -------------------------------------------------------------------------------- /src/Invocation/Invocable.php: -------------------------------------------------------------------------------- 1 | $arguments The arguments. 21 | * 22 | * @return mixed The result of invocation. 23 | * @throws Throwable If an error occurs. 24 | */ 25 | public function invokeWith($arguments = []); 26 | 27 | /** 28 | * Invoke this object. 29 | * 30 | * @param mixed ...$arguments The arguments. 31 | * 32 | * @return mixed The result of invocation. 33 | * @throws Throwable If an error occurs. 34 | */ 35 | public function invoke(...$arguments); 36 | 37 | /** 38 | * Invoke this object. 39 | * 40 | * @param mixed ...$arguments The arguments. 41 | * 42 | * @return mixed The result of invocation. 43 | * @throws Throwable If an error occurs. 44 | */ 45 | public function __invoke(...$arguments); 46 | } 47 | -------------------------------------------------------------------------------- /src/Invocation/InvocableInspector.php: -------------------------------------------------------------------------------- 1 | method(); 33 | } 34 | 35 | $callback = $callback->callback(); 36 | } 37 | 38 | if (is_array($callback)) { 39 | return new ReflectionMethod($callback[0], $callback[1]); 40 | } 41 | 42 | if (is_string($callback) && false !== strpos($callback, '::')) { 43 | list($className, $methodName) = explode('::', $callback); 44 | 45 | return new ReflectionMethod($className, $methodName); 46 | } 47 | 48 | if (is_object($callback) && !$callback instanceof Closure) { 49 | return new ReflectionMethod($callback, '__invoke'); 50 | } 51 | 52 | /** @var string $callback */ 53 | 54 | return new ReflectionFunction($callback); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Invocation/Invoker.php: -------------------------------------------------------------------------------- 1 | invokeWith($arguments); 28 | } 29 | 30 | $arguments = $arguments->all(); 31 | 32 | return $callback(...$arguments); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Invocation/WrappedInvocable.php: -------------------------------------------------------------------------------- 1 | isAnonymous; 22 | } 23 | 24 | /** 25 | * Get the callback. 26 | * 27 | * @return ?callable The callback. 28 | */ 29 | public function callback(): ?callable 30 | { 31 | return $this->callback; 32 | } 33 | 34 | /** 35 | * Set the label. 36 | * 37 | * @param string $label The label. 38 | * 39 | * @return $this This invocable. 40 | */ 41 | public function setLabel(string $label): WrappedInvocable 42 | { 43 | $this->label = $label; 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * Get the label. 50 | * 51 | * @return string The label. 52 | */ 53 | public function label(): string 54 | { 55 | return $this->label; 56 | } 57 | 58 | /** 59 | * Invoke this object. 60 | * 61 | * Does not support named arguments. 62 | * 63 | * @param mixed ...$arguments The arguments. 64 | * 65 | * @return mixed The result of invocation. 66 | * @throws Throwable If an error occurs. 67 | */ 68 | public function invoke(...$arguments) 69 | { 70 | /** @var array $arguments */ 71 | 72 | return $this->invokeWith($arguments); 73 | } 74 | 75 | /** 76 | * Invoke this object. 77 | * 78 | * Does not support named arguments. 79 | * 80 | * @param mixed ...$arguments The arguments. 81 | * 82 | * @return mixed The result of invocation. 83 | * @throws Throwable If an error occurs. 84 | */ 85 | public function __invoke(...$arguments) 86 | { 87 | /** @var array $arguments */ 88 | 89 | return $this->invokeWith($arguments); 90 | } 91 | 92 | /** 93 | * Invoke this object. 94 | * 95 | * This method supports reference parameters. 96 | * 97 | * @param Arguments|array $arguments The arguments. 98 | * 99 | * @return mixed The result of invocation. 100 | * @throws Throwable If an error occurs. 101 | */ 102 | abstract public function invokeWith($arguments = []); 103 | 104 | /** 105 | * @var ?callable 106 | */ 107 | protected $callback; 108 | 109 | /** 110 | * @var bool 111 | */ 112 | protected $isAnonymous = false; 113 | 114 | /** 115 | * @var string 116 | */ 117 | protected $label = ''; 118 | } 119 | -------------------------------------------------------------------------------- /src/Matcher/AnyMatcher.php: -------------------------------------------------------------------------------- 1 | '; 36 | } 37 | 38 | /** 39 | * Describe this matcher. 40 | * 41 | * @return string The description. 42 | */ 43 | public function __toString(): string 44 | { 45 | return ''; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Matcher/Exception/UndefinedTypeException.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | 23 | parent::__construct( 24 | sprintf('Undefined type %s.', var_export($type, true)) 25 | ); 26 | } 27 | 28 | /** 29 | * Get the type. 30 | * 31 | * @return string The type. 32 | */ 33 | public function type(): string 34 | { 35 | return $this->type; 36 | } 37 | 38 | /** 39 | * @var string 40 | */ 41 | private $type; 42 | } 43 | -------------------------------------------------------------------------------- /src/Matcher/InstanceOfMatcher.php: -------------------------------------------------------------------------------- 1 | type = $type; 24 | 25 | $atoms = explode('\\', $type); 26 | 27 | /** @var string */ 28 | $shortType = array_pop($atoms); 29 | $this->shortType = $shortType; 30 | } 31 | 32 | /** 33 | * Get the type. 34 | * 35 | * @return string The type. 36 | */ 37 | public function type(): string 38 | { 39 | return $this->type; 40 | } 41 | 42 | /** 43 | * Returns `true` if `$value` matches this matcher's criteria. 44 | * 45 | * @param mixed $value The value to check. 46 | * 47 | * @return bool True if the value matches. 48 | */ 49 | public function matches($value): bool 50 | { 51 | if (!interface_exists($this->type) && !class_exists($this->type)) { 52 | throw new UndefinedTypeException($this->type); 53 | } 54 | 55 | return $value instanceof $this->type; 56 | } 57 | 58 | /** 59 | * Describe this matcher. 60 | * 61 | * @param ?Exporter $exporter The exporter to use. 62 | * 63 | * @return string The description. 64 | */ 65 | public function describe(Exporter $exporter = null): string 66 | { 67 | return 'shortType . '>'; 68 | } 69 | 70 | /** 71 | * Describe this matcher. 72 | * 73 | * @return string The description. 74 | */ 75 | public function __toString(): string 76 | { 77 | return 'shortType . '>'; 78 | } 79 | 80 | /** 81 | * @var string 82 | */ 83 | private $type; 84 | 85 | /** 86 | * @var string 87 | */ 88 | private $shortType; 89 | } 90 | -------------------------------------------------------------------------------- /src/Matcher/Matcher.php: -------------------------------------------------------------------------------- 1 | The matcher class names. 23 | */ 24 | public function matcherClassNames(): array; 25 | 26 | /** 27 | * Wrap the supplied third party matcher. 28 | * 29 | * @param object $matcher The matcher to wrap. 30 | * 31 | * @return Matcher The wrapped matcher. 32 | */ 33 | public function wrapMatcher(object $matcher): Matcher; 34 | } 35 | -------------------------------------------------------------------------------- /src/Matcher/MatcherResult.php: -------------------------------------------------------------------------------- 1 | $matcherMatches The matcher results. 17 | * @param array $argumentMatches The argument results. 18 | */ 19 | public function __construct( 20 | bool $isMatch, 21 | array $matcherMatches, 22 | array $argumentMatches 23 | ) { 24 | $this->isMatch = $isMatch; 25 | $this->matcherMatches = $matcherMatches; 26 | $this->argumentMatches = $argumentMatches; 27 | } 28 | 29 | /** 30 | * @var bool 31 | */ 32 | public $isMatch; 33 | 34 | /** 35 | * @var array 36 | */ 37 | public $matcherMatches; 38 | 39 | /** 40 | * @var array 41 | */ 42 | public $argumentMatches; 43 | } 44 | -------------------------------------------------------------------------------- /src/Matcher/WildcardMatcher.php: -------------------------------------------------------------------------------- 1 | matcher = $matcher; 29 | $this->minimumArguments = $minimumArguments; 30 | $this->maximumArguments = $maximumArguments; 31 | } 32 | 33 | /** 34 | * Get the matcher to use for each argument. 35 | * 36 | * @return Matcher The matcher. 37 | */ 38 | public function matcher(): Matcher 39 | { 40 | return $this->matcher; 41 | } 42 | 43 | /** 44 | * Get the minimum number of arguments to match. 45 | * 46 | * @return int The minimum number of arguments. 47 | */ 48 | public function minimumArguments(): int 49 | { 50 | return $this->minimumArguments; 51 | } 52 | 53 | /** 54 | * Get the maximum number of arguments to match. 55 | * 56 | * @return int The maximum number of arguments. 57 | */ 58 | public function maximumArguments(): int 59 | { 60 | return $this->maximumArguments; 61 | } 62 | 63 | /** 64 | * Describe this matcher. 65 | * 66 | * @param ?Exporter $exporter The exporter to use. 67 | * 68 | * @return string The description. 69 | */ 70 | public function describe(Exporter $exporter = null): string 71 | { 72 | $matcherDescription = $this->matcher->describe($exporter); 73 | 74 | if (0 === $this->minimumArguments) { 75 | if ($this->maximumArguments < 0) { 76 | return sprintf('%s*', $matcherDescription); 77 | } else { 78 | return sprintf( 79 | '%s{,%d}', 80 | $matcherDescription, 81 | $this->maximumArguments 82 | ); 83 | } 84 | } elseif ($this->maximumArguments < 0) { 85 | return sprintf( 86 | '%s{%d,}', 87 | $matcherDescription, 88 | $this->minimumArguments 89 | ); 90 | } elseif ($this->minimumArguments === $this->maximumArguments) { 91 | return sprintf( 92 | '%s{%d}', 93 | $matcherDescription, 94 | $this->minimumArguments 95 | ); 96 | } 97 | 98 | return sprintf( 99 | '%s{%d,%d}', 100 | $matcherDescription, 101 | $this->minimumArguments, 102 | $this->maximumArguments 103 | ); 104 | } 105 | 106 | /** 107 | * Describe this matcher. 108 | * 109 | * @return string The description. 110 | */ 111 | public function __toString(): string 112 | { 113 | return $this->describe(); 114 | } 115 | 116 | /** 117 | * Always returns false. 118 | * 119 | * @param mixed $value The value to check. 120 | * 121 | * @return false For all values. 122 | */ 123 | public function matches($value): bool 124 | { 125 | return false; 126 | } 127 | 128 | /** 129 | * @var Matcher 130 | */ 131 | private $matcher; 132 | 133 | /** 134 | * @var int 135 | */ 136 | private $minimumArguments; 137 | 138 | /** 139 | * @var int 140 | */ 141 | private $maximumArguments; 142 | } 143 | -------------------------------------------------------------------------------- /src/Mock/Builder/Method/CustomMethodDefinition.php: -------------------------------------------------------------------------------- 1 | isStatic = $isStatic; 29 | $this->name = $name; 30 | $this->callback = $callback; 31 | $this->method = $method; 32 | } 33 | 34 | /** 35 | * Returns true if this method is callable. 36 | * 37 | * @return bool True if this method is callable. 38 | */ 39 | public function isCallable(): bool 40 | { 41 | return true; 42 | } 43 | 44 | /** 45 | * Returns true if this method is static. 46 | * 47 | * @return bool True if this method is static. 48 | */ 49 | public function isStatic(): bool 50 | { 51 | return $this->isStatic; 52 | } 53 | 54 | /** 55 | * Returns true if this method is custom. 56 | * 57 | * @return bool True if this method is custom. 58 | */ 59 | public function isCustom(): bool 60 | { 61 | return true; 62 | } 63 | 64 | /** 65 | * Get the access level. 66 | * 67 | * @return string The access level. 68 | */ 69 | public function accessLevel(): string 70 | { 71 | return 'public'; 72 | } 73 | 74 | /** 75 | * Get the name. 76 | * 77 | * @return string The name. 78 | */ 79 | public function name(): string 80 | { 81 | return $this->name; 82 | } 83 | 84 | /** 85 | * Get the method. 86 | * 87 | * @return ReflectionFunctionAbstract The method. 88 | */ 89 | public function method(): ReflectionFunctionAbstract 90 | { 91 | return $this->method; 92 | } 93 | 94 | /** 95 | * Get the callback. 96 | * 97 | * @return callable The callback, or null if this is a real method. 98 | */ 99 | public function callback(): ?callable 100 | { 101 | return $this->callback; 102 | } 103 | 104 | /** 105 | * @var bool 106 | */ 107 | private $isStatic; 108 | 109 | /** 110 | * @var string 111 | */ 112 | private $name; 113 | 114 | /** 115 | * @var callable 116 | */ 117 | private $callback; 118 | 119 | /** 120 | * @var ReflectionFunctionAbstract 121 | */ 122 | private $method; 123 | } 124 | -------------------------------------------------------------------------------- /src/Mock/Builder/Method/MethodDefinition.php: -------------------------------------------------------------------------------- 1 | method = $method; 24 | $this->name = $name; 25 | $this->isCallable = !$this->method->isAbstract(); 26 | $this->isStatic = $this->method->isStatic(); 27 | 28 | if ($this->method->isPublic()) { 29 | $this->accessLevel = 'public'; 30 | } else { 31 | $this->accessLevel = 'protected'; 32 | } 33 | } 34 | 35 | /** 36 | * Returns true if this method is callable. 37 | * 38 | * @return bool True if this method is callable. 39 | */ 40 | public function isCallable(): bool 41 | { 42 | return $this->isCallable; 43 | } 44 | 45 | /** 46 | * Returns true if this method is static. 47 | * 48 | * @return bool True if this method is static. 49 | */ 50 | public function isStatic(): bool 51 | { 52 | return $this->isStatic; 53 | } 54 | 55 | /** 56 | * Returns true if this method is custom. 57 | * 58 | * @return bool True if this method is custom. 59 | */ 60 | public function isCustom(): bool 61 | { 62 | return false; 63 | } 64 | 65 | /** 66 | * Get the access level. 67 | * 68 | * @return string The access level. 69 | */ 70 | public function accessLevel(): string 71 | { 72 | return $this->accessLevel; 73 | } 74 | 75 | /** 76 | * Get the name. 77 | * 78 | * @return string The name. 79 | */ 80 | public function name(): string 81 | { 82 | return $this->name; 83 | } 84 | 85 | /** 86 | * Get the method. 87 | * 88 | * @return ReflectionMethod The method. 89 | */ 90 | public function method(): ReflectionFunctionAbstract 91 | { 92 | return $this->method; 93 | } 94 | 95 | /** 96 | * Get the callback. 97 | * 98 | * @return ?callable The callback, or null if this is a real method. 99 | */ 100 | public function callback(): ?callable 101 | { 102 | return null; 103 | } 104 | 105 | /** 106 | * @var ReflectionMethod 107 | */ 108 | private $method; 109 | 110 | /** 111 | * @var string 112 | */ 113 | private $name; 114 | 115 | /** 116 | * @var bool 117 | */ 118 | private $isCallable; 119 | 120 | /** 121 | * @var bool 122 | */ 123 | private $isStatic; 124 | 125 | /** 126 | * @var string 127 | */ 128 | private $accessLevel; 129 | } 130 | -------------------------------------------------------------------------------- /src/Mock/Builder/Method/TraitMethodDefinition.php: -------------------------------------------------------------------------------- 1 | mockGenerator = $mockGenerator; 35 | $this->mockFactory = $mockFactory; 36 | $this->handleFactory = $handleFactory; 37 | $this->invocableInspector = $invocableInspector; 38 | $this->featureDetector = $featureDetector; 39 | } 40 | 41 | /** 42 | * Create a new mock builder. 43 | * 44 | * Each value in `$types` can be either a class name, or an ad hoc mock 45 | * definition. If only a single type is being mocked, the class name or 46 | * definition can be passed without being wrapped in an array. 47 | * 48 | * @param mixed $types The types to mock. 49 | * 50 | * @return MockBuilder The mock builder. 51 | */ 52 | public function create($types = []): MockBuilder 53 | { 54 | return new MockBuilder( 55 | $types, 56 | $this->mockGenerator, 57 | $this->mockFactory, 58 | $this->handleFactory, 59 | $this->invocableInspector, 60 | $this->featureDetector 61 | ); 62 | } 63 | 64 | /** 65 | * @var MockGenerator 66 | */ 67 | private $mockGenerator; 68 | 69 | /** 70 | * @var MockFactory 71 | */ 72 | private $mockFactory; 73 | 74 | /** 75 | * @var HandleFactory 76 | */ 77 | private $handleFactory; 78 | 79 | /** 80 | * @var InvocableInspector 81 | */ 82 | private $invocableInspector; 83 | 84 | /** 85 | * @var FeatureDetector 86 | */ 87 | private $featureDetector; 88 | } 89 | -------------------------------------------------------------------------------- /src/Mock/Exception/AnonymousClassException.php: -------------------------------------------------------------------------------- 1 | className = $className; 23 | 24 | parent::__construct( 25 | sprintf( 26 | 'Class %s is already defined.', 27 | var_export($className, true) 28 | ) 29 | ); 30 | } 31 | 32 | /** 33 | * Get the class name. 34 | * 35 | * @return string The class name. 36 | */ 37 | public function className(): string 38 | { 39 | return $this->className; 40 | } 41 | 42 | /** 43 | * @var string 44 | */ 45 | private $className; 46 | } 47 | -------------------------------------------------------------------------------- /src/Mock/Exception/FinalClassException.php: -------------------------------------------------------------------------------- 1 | className = $className; 23 | 24 | parent::__construct( 25 | sprintf( 26 | 'Unable to extend final class %s.', 27 | var_export($className, true) 28 | ) 29 | ); 30 | } 31 | 32 | /** 33 | * Get the class name. 34 | * 35 | * @return string The class name. 36 | */ 37 | public function className(): string 38 | { 39 | return $this->className; 40 | } 41 | 42 | /** 43 | * @var string 44 | */ 45 | private $className; 46 | } 47 | -------------------------------------------------------------------------------- /src/Mock/Exception/FinalMethodStubException.php: -------------------------------------------------------------------------------- 1 | className = $className; 24 | $this->name = $name; 25 | 26 | parent::__construct( 27 | sprintf( 28 | 'The method %s::%s() cannot be stubbed because it is final.', 29 | $className, 30 | $name 31 | ) 32 | ); 33 | } 34 | 35 | /** 36 | * Get the class name. 37 | * 38 | * @return string The class name. 39 | */ 40 | public function className(): string 41 | { 42 | return $this->className; 43 | } 44 | 45 | /** 46 | * Get the method name. 47 | * 48 | * @return string The method name. 49 | */ 50 | public function name(): string 51 | { 52 | return $this->name; 53 | } 54 | 55 | /** 56 | * @var string 57 | */ 58 | private $className; 59 | 60 | /** 61 | * @var string 62 | */ 63 | private $name; 64 | } 65 | -------------------------------------------------------------------------------- /src/Mock/Exception/FinalizedMockException.php: -------------------------------------------------------------------------------- 1 | className = $className; 23 | 24 | parent::__construct( 25 | sprintf('Invalid class name %s.', var_export($className, true)) 26 | ); 27 | } 28 | 29 | /** 30 | * Get the class name. 31 | * 32 | * @return mixed The class name. 33 | */ 34 | public function className() 35 | { 36 | return $this->className; 37 | } 38 | 39 | /** 40 | * @var mixed 41 | */ 42 | private $className; 43 | } 44 | -------------------------------------------------------------------------------- /src/Mock/Exception/InvalidDefinitionException.php: -------------------------------------------------------------------------------- 1 | name = $name; 24 | $this->value = $value; 25 | 26 | parent::__construct( 27 | sprintf( 28 | 'Invalid mock definition %s: (%s).', 29 | var_export($name, true), 30 | gettype($value) 31 | ) 32 | ); 33 | } 34 | 35 | /** 36 | * Get the name. 37 | * 38 | * @return mixed The name. 39 | */ 40 | public function name() 41 | { 42 | return $this->name; 43 | } 44 | 45 | /** 46 | * Get the value. 47 | * 48 | * @return mixed The value. 49 | */ 50 | public function value() 51 | { 52 | return $this->value; 53 | } 54 | 55 | /** 56 | * @var mixed 57 | */ 58 | private $name; 59 | 60 | /** 61 | * @var mixed 62 | */ 63 | private $value; 64 | } 65 | -------------------------------------------------------------------------------- /src/Mock/Exception/InvalidMockClassException.php: -------------------------------------------------------------------------------- 1 | value = $value; 23 | 24 | parent::__construct( 25 | sprintf( 26 | 'Value of type %s is not a mock class.', 27 | var_export(gettype($value), true) 28 | ) 29 | ); 30 | } 31 | 32 | /** 33 | * Get the value. 34 | * 35 | * @return mixed The value. 36 | */ 37 | public function value() 38 | { 39 | return $this->value; 40 | } 41 | 42 | /** 43 | * @var mixed 44 | */ 45 | private $value; 46 | } 47 | -------------------------------------------------------------------------------- /src/Mock/Exception/InvalidMockException.php: -------------------------------------------------------------------------------- 1 | value = $value; 23 | 24 | if (is_object($value)) { 25 | $message = sprintf( 26 | 'Object of type %s is not a mock.', 27 | var_export(get_class($value), true) 28 | ); 29 | } else { 30 | $message = sprintf( 31 | 'Value of type %s is not a mock.', 32 | var_export(gettype($value), true) 33 | ); 34 | } 35 | 36 | parent::__construct($message); 37 | } 38 | 39 | /** 40 | * Get the value. 41 | * 42 | * @return mixed The value. 43 | */ 44 | public function value() 45 | { 46 | return $this->value; 47 | } 48 | 49 | /** 50 | * @var mixed 51 | */ 52 | private $value; 53 | } 54 | -------------------------------------------------------------------------------- /src/Mock/Exception/InvalidTypeException.php: -------------------------------------------------------------------------------- 1 | type = $type; 25 | 26 | if (is_string($type)) { 27 | $message = sprintf('Undefined type %s.', var_export($type, true)); 28 | } else { 29 | $message = sprintf( 30 | 'Unable to add type of type %s.', 31 | var_export(gettype($type), true) 32 | ); 33 | } 34 | 35 | parent::__construct($message, 0, $cause); 36 | } 37 | 38 | /** 39 | * Get the type. 40 | * 41 | * @return mixed The type. 42 | */ 43 | public function type() 44 | { 45 | return $this->type; 46 | } 47 | 48 | /** 49 | * @var mixed 50 | */ 51 | private $type; 52 | } 53 | -------------------------------------------------------------------------------- /src/Mock/Exception/MockException.php: -------------------------------------------------------------------------------- 1 | definition = $definition; 34 | $this->source = $source; 35 | $this->error = $error; 36 | 37 | $lines = explode(PHP_EOL, $source); 38 | 39 | if (null === $error) { 40 | $message = sprintf( 41 | 'Mock class %s generation failed.%sRelevant lines:%%s', 42 | $className, 43 | PHP_EOL 44 | ); 45 | $errorLineNumber = null; 46 | } else { 47 | /** @var int */ 48 | $errorLineNumber = $error['line']; 49 | $startLine = $errorLineNumber - 4; 50 | $contextLineCount = 7; 51 | 52 | if ($startLine < 0) { 53 | $contextLineCount += $startLine; 54 | $startLine = 0; 55 | } 56 | 57 | $lines = array_slice($lines, $startLine, $contextLineCount, true); 58 | 59 | $message = sprintf( 60 | 'Mock class %s generation failed: ' . 61 | '%s in generated code on line %d.%s' . 62 | 'Relevant lines:%%s', 63 | $className, 64 | $error['message'], 65 | $errorLineNumber, 66 | PHP_EOL 67 | ); 68 | } 69 | 70 | end($lines); 71 | $lineNumber = key($lines); 72 | $padSize = strlen((string) ($lineNumber + 1)) + 4; 73 | $renderedLines = ''; 74 | 75 | foreach ($lines as $lineNumber => $line) { 76 | if (null !== $errorLineNumber) { 77 | $highlight = $lineNumber + 1 === $errorLineNumber; 78 | } else { 79 | $highlight = false; 80 | } 81 | 82 | $renderedLines .= sprintf( 83 | '%s%s%s %s', 84 | PHP_EOL, 85 | str_pad( 86 | (string) ($lineNumber + 1), 87 | $padSize, 88 | ' ', 89 | STR_PAD_LEFT 90 | ), 91 | $highlight ? ':' : ' ', 92 | $line 93 | ); 94 | } 95 | 96 | parent::__construct(sprintf($message, $renderedLines), 0, $cause); 97 | } 98 | 99 | /** 100 | * Get the definition. 101 | * 102 | * @return MockDefinition The definition. 103 | */ 104 | public function definition(): MockDefinition 105 | { 106 | return $this->definition; 107 | } 108 | 109 | /** 110 | * Get the generated source code. 111 | * 112 | * @return string The generated source code. 113 | */ 114 | public function source(): string 115 | { 116 | return $this->source; 117 | } 118 | 119 | /** 120 | * Get the error details. 121 | * 122 | * @return ?array The error details. 123 | */ 124 | public function error(): ?array 125 | { 126 | return $this->error; 127 | } 128 | 129 | /** 130 | * @var MockDefinition 131 | */ 132 | private $definition; 133 | 134 | /** 135 | * @var string 136 | */ 137 | private $source; 138 | 139 | /** 140 | * @var ?array 141 | */ 142 | private $error; 143 | } 144 | -------------------------------------------------------------------------------- /src/Mock/Exception/MultipleInheritanceException.php: -------------------------------------------------------------------------------- 1 | $classNames The class names. 19 | */ 20 | public function __construct(array $classNames) 21 | { 22 | $this->classNames = $classNames; 23 | 24 | parent::__construct( 25 | sprintf( 26 | 'Unable to extend %s simultaneously.', 27 | implode( 28 | ' and ', 29 | array_map( 30 | function ($className) { 31 | return var_export($className, true); 32 | }, 33 | $classNames 34 | ) 35 | ) 36 | ) 37 | ); 38 | } 39 | 40 | /** 41 | * Get the class names. 42 | * 43 | * @return array The class names. 44 | */ 45 | public function classNames(): array 46 | { 47 | return $this->classNames; 48 | } 49 | 50 | /** 51 | * @var array 52 | */ 53 | private $classNames; 54 | } 55 | -------------------------------------------------------------------------------- /src/Mock/Exception/NonMockClassException.php: -------------------------------------------------------------------------------- 1 | className = $className; 25 | 26 | parent::__construct( 27 | sprintf( 28 | 'The class %s is not a mock class.', 29 | var_export($className, true) 30 | ), 31 | 0, 32 | $cause 33 | ); 34 | } 35 | 36 | /** 37 | * Get the class name. 38 | * 39 | * @return string The class name. 40 | */ 41 | public function className(): string 42 | { 43 | return $this->className; 44 | } 45 | 46 | /** 47 | * @var string 48 | */ 49 | private $className; 50 | } 51 | -------------------------------------------------------------------------------- /src/Mock/Exception/UndefinedMethodStubException.php: -------------------------------------------------------------------------------- 1 | className = $className; 24 | $this->name = $name; 25 | 26 | parent::__construct( 27 | sprintf( 28 | 'The requested method stub %s::%s() does not exist.', 29 | $className, 30 | $name 31 | ) 32 | ); 33 | } 34 | 35 | /** 36 | * Get the class name. 37 | * 38 | * @return string The class name. 39 | */ 40 | public function className(): string 41 | { 42 | return $this->className; 43 | } 44 | 45 | /** 46 | * Get the method name. 47 | * 48 | * @return string The method name. 49 | */ 50 | public function name(): string 51 | { 52 | return $this->name; 53 | } 54 | 55 | /** 56 | * @var string 57 | */ 58 | private $className; 59 | 60 | /** 61 | * @var string 62 | */ 63 | private $name; 64 | } 65 | -------------------------------------------------------------------------------- /src/Mock/Handle/Handle.php: -------------------------------------------------------------------------------- 1 | The class. 24 | */ 25 | public function class(): ReflectionClass; 26 | 27 | /** 28 | * Get the class name. 29 | * 30 | * @return string The class name. 31 | */ 32 | public function className(): string; 33 | 34 | /** 35 | * Turn the mock into a full mock. 36 | * 37 | * @return $this This handle. 38 | */ 39 | public function full(): self; 40 | 41 | /** 42 | * Turn the mock into a partial mock. 43 | * 44 | * @return $this This handle. 45 | */ 46 | public function partial(): self; 47 | 48 | /** 49 | * Use the supplied object as the implementation for all methods of the 50 | * mock. 51 | * 52 | * This method may help when partial mocking of a particular implementation 53 | * is not possible; as in the case of a final class. 54 | * 55 | * @param object $object The object to use. 56 | * 57 | * @return $this This handle. 58 | */ 59 | public function proxy($object): self; 60 | 61 | /** 62 | * Set the callback to use when creating a default answer. 63 | * 64 | * @param callable $defaultAnswerCallback The default answer callback. 65 | * 66 | * @return $this This handle. 67 | */ 68 | public function setDefaultAnswerCallback( 69 | callable $defaultAnswerCallback 70 | ): self; 71 | 72 | /** 73 | * Get the default answer callback. 74 | * 75 | * @return callable The default answer callback. 76 | */ 77 | public function defaultAnswerCallback(): callable; 78 | 79 | /** 80 | * Get a stub verifier. 81 | * 82 | * @param string $name The method name. 83 | * @param bool $isNewRule True if a new rule should be started. 84 | * 85 | * @return StubVerifier The stub verifier. 86 | * @throws MockException If the stub does not exist. 87 | */ 88 | public function stub(string $name, bool $isNewRule = true): StubVerifier; 89 | 90 | /** 91 | * Get a stub verifier. 92 | * 93 | * Using this method will always start a new rule. 94 | * 95 | * @param string $name The method name. 96 | * 97 | * @return StubVerifier The stub verifier. 98 | * @throws MockException If the stub does not exist. 99 | */ 100 | public function __get(string $name): StubVerifier; 101 | 102 | /** 103 | * Checks if there was no interaction with the mock. 104 | * 105 | * @return ?EventCollection The result. 106 | */ 107 | public function checkNoInteraction(): ?EventCollection; 108 | 109 | /** 110 | * Record an assertion failure unless there was no interaction with the mock. 111 | * 112 | * @return ?EventCollection The result, or null if the assertion recorder does not throw exceptions. 113 | * @throws Throwable If the assertion fails, and the assertion recorder throws exceptions. 114 | */ 115 | public function noInteraction(): ?EventCollection; 116 | 117 | /** 118 | * Stop recording calls. 119 | * 120 | * @return $this This handle. 121 | */ 122 | public function stopRecording(): self; 123 | 124 | /** 125 | * Start recording calls. 126 | * 127 | * @return $this This handle. 128 | */ 129 | public function startRecording(): self; 130 | 131 | /** 132 | * Get a spy. 133 | * 134 | * @param string $name The method name. 135 | * 136 | * @return Spy The spy. 137 | * @throws MockException If the spy does not exist. 138 | */ 139 | public function spy(string $name): Spy; 140 | 141 | /** 142 | * Get the handle state. 143 | * 144 | * @return stdClass The state. 145 | */ 146 | public function state(): stdClass; 147 | } 148 | -------------------------------------------------------------------------------- /src/Mock/Handle/StaticHandle.php: -------------------------------------------------------------------------------- 1 | $class The class. 30 | * @param stdClass $state The state. 31 | * @param StubFactory $stubFactory The stub factory to use. 32 | * @param StubVerifierFactory $stubVerifierFactory The stub verifier factory to use. 33 | * @param EmptyValueFactory $emptyValueFactory The empty value factory to use. 34 | * @param AssertionRenderer $assertionRenderer The assertion renderer to use. 35 | * @param AssertionRecorder $assertionRecorder The assertion recorder to use. 36 | * @param Invoker $invoker The invoker to use. 37 | */ 38 | public function __construct( 39 | MockDefinition $mockDefinition, 40 | ReflectionClass $class, 41 | stdClass $state, 42 | StubFactory $stubFactory, 43 | StubVerifierFactory $stubVerifierFactory, 44 | EmptyValueFactory $emptyValueFactory, 45 | AssertionRenderer $assertionRenderer, 46 | AssertionRecorder $assertionRecorder, 47 | Invoker $invoker 48 | ) { 49 | if ($class->hasMethod('_callParentStatic')) { 50 | $callParentMethod = $class->getMethod('_callParentStatic'); 51 | $callParentMethod->setAccessible(true); 52 | } else { 53 | $callParentMethod = null; 54 | } 55 | 56 | if ($class->hasMethod('_callTraitStatic')) { 57 | $callTraitMethod = $class->getMethod('_callTraitStatic'); 58 | $callTraitMethod->setAccessible(true); 59 | } else { 60 | $callTraitMethod = null; 61 | } 62 | 63 | if ($class->hasMethod('_callMagicStatic')) { 64 | $callMagicMethod = $class->getMethod('_callMagicStatic'); 65 | $callMagicMethod->setAccessible(true); 66 | } else { 67 | $callMagicMethod = null; 68 | } 69 | 70 | $this->constructHandle( 71 | $mockDefinition, 72 | $class, 73 | $state, 74 | $callParentMethod, 75 | $callTraitMethod, 76 | $callMagicMethod, 77 | null, 78 | $stubFactory, 79 | $stubVerifierFactory, 80 | $emptyValueFactory, 81 | $assertionRenderer, 82 | $assertionRecorder, 83 | $invoker 84 | ); 85 | } 86 | 87 | /** 88 | * Use the supplied object as the implementation for all methods of the 89 | * mock. 90 | * 91 | * This method may help when partial mocking of a particular implementation 92 | * is not possible; as in the case of a final class. 93 | * 94 | * @param object $object The object to use. 95 | * 96 | * @return $this This handle. 97 | */ 98 | public function proxy($object): Handle 99 | { 100 | $reflector = new ReflectionObject($object); 101 | 102 | foreach ($reflector->getMethods() as $method) { 103 | if (!$method->isStatic() || $method->isPrivate()) { 104 | continue; 105 | } 106 | 107 | $name = $method->getName(); 108 | 109 | if ($this->class->hasMethod($name)) { 110 | $method->setAccessible(true); 111 | 112 | $this->stub($name)->doesWith( 113 | function ($arguments) use ($method, $object) { 114 | return $method->invokeArgs($object, $arguments->all()); 115 | }, 116 | [], 117 | false, 118 | true, 119 | false 120 | ); 121 | } 122 | } 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * Limits the output displayed when `var_dump` is used. 129 | * 130 | * @return array The contents to export. 131 | */ 132 | public function __debugInfo(): array 133 | { 134 | return ['class' => $this->class]; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Mock/Handle/StaticHandleRegistry.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public static $handles = []; 16 | } 17 | -------------------------------------------------------------------------------- /src/Mock/Method/WrappedCustomMethod.php: -------------------------------------------------------------------------------- 1 | customCallback = $customCallback; 35 | $this->invoker = $invoker; 36 | 37 | $this->constructWrappedMethod($method, $handle); 38 | } 39 | 40 | /** 41 | * Get the custom callback. 42 | * 43 | * @return callable The custom callback. 44 | */ 45 | public function customCallback(): callable 46 | { 47 | return $this->customCallback; 48 | } 49 | 50 | /** 51 | * Invoke this object. 52 | * 53 | * This method supports reference parameters. 54 | * 55 | * @param Arguments|array $arguments The arguments. 56 | * 57 | * @return mixed The result of invocation. 58 | * @throws Throwable If an error occurs. 59 | */ 60 | public function invokeWith($arguments = []) 61 | { 62 | if (!$arguments instanceof Arguments) { 63 | $arguments = new Arguments($arguments); 64 | } 65 | 66 | return $this->invoker->callWith($this->customCallback, $arguments); 67 | } 68 | 69 | /** 70 | * @var callable 71 | */ 72 | private $customCallback; 73 | 74 | /** 75 | * @var Invoker 76 | */ 77 | private $invoker; 78 | } 79 | -------------------------------------------------------------------------------- /src/Mock/Method/WrappedMagicMethod.php: -------------------------------------------------------------------------------- 1 | callMagicMethod = $callMagicMethod; 42 | $this->method = $method; 43 | $this->name = $name; 44 | $this->isUncallable = $isUncallable; 45 | $this->handle = $handle; 46 | $this->exception = $exception; 47 | $this->returnValue = $returnValue; 48 | 49 | if ($handle instanceof StaticHandle) { 50 | $this->mock = null; 51 | } elseif ($handle instanceof InstanceHandle) { 52 | $this->mock = $handle->get(); 53 | } 54 | } 55 | 56 | /** 57 | * Get the method. 58 | * 59 | * @return ReflectionMethod The method. 60 | */ 61 | public function callMagicMethod(): ReflectionMethod 62 | { 63 | return $this->callMagicMethod; 64 | } 65 | 66 | /** 67 | * Returns true if uncallable. 68 | * 69 | * @return bool True if uncallable. 70 | */ 71 | public function isUncallable(): bool 72 | { 73 | return $this->isUncallable; 74 | } 75 | 76 | /** 77 | * Invoke this object. 78 | * 79 | * This method supports reference parameters. 80 | * 81 | * @param Arguments|array $arguments The arguments. 82 | * 83 | * @return mixed The result of invocation. 84 | * @throws Throwable If an error occurs. 85 | */ 86 | public function invokeWith($arguments = []) 87 | { 88 | if ($this->exception) { 89 | throw $this->exception; 90 | } 91 | 92 | if ($this->isUncallable) { 93 | return $this->returnValue; 94 | } 95 | 96 | if (!$arguments instanceof Arguments) { 97 | $arguments = new Arguments($arguments); 98 | } 99 | 100 | return $this->callMagicMethod 101 | ->invoke($this->mock, $this->name, $arguments); 102 | } 103 | 104 | /** 105 | * @var string 106 | */ 107 | private $name; 108 | 109 | /** 110 | * @var ReflectionMethod 111 | */ 112 | private $callMagicMethod; 113 | 114 | /** 115 | * @var bool 116 | */ 117 | private $isUncallable; 118 | 119 | /** 120 | * @var ?Throwable 121 | */ 122 | private $exception; 123 | 124 | /** 125 | * @var mixed 126 | */ 127 | private $returnValue; 128 | } 129 | -------------------------------------------------------------------------------- /src/Mock/Method/WrappedMethod.php: -------------------------------------------------------------------------------- 1 | method; 29 | } 30 | 31 | /** 32 | * Get the name. 33 | * 34 | * @return string The name. 35 | */ 36 | public function name(): string 37 | { 38 | return $this->name; 39 | } 40 | 41 | /** 42 | * Get the handle. 43 | * 44 | * @return Handle The handle. 45 | */ 46 | public function handle(): Handle 47 | { 48 | return $this->handle; 49 | } 50 | 51 | /** 52 | * Get the mock. 53 | * 54 | * @return ?Mock The mock. 55 | */ 56 | public function mock(): ?Mock 57 | { 58 | return $this->mock; 59 | } 60 | 61 | private function constructWrappedMethod( 62 | ReflectionMethod $method, 63 | Handle $handle 64 | ): void { 65 | $this->method = $method; 66 | $this->handle = $handle; 67 | $this->name = $method->getName(); 68 | 69 | if ($handle instanceof StaticHandle) { 70 | $this->mock = null; 71 | } elseif ($handle instanceof InstanceHandle) { 72 | $this->mock = $handle->get(); 73 | } 74 | } 75 | 76 | /** 77 | * @var ReflectionMethod 78 | */ 79 | private $method; 80 | 81 | /** 82 | * @var Handle 83 | */ 84 | private $handle; 85 | 86 | /** 87 | * @var ?Mock 88 | */ 89 | private $mock; 90 | 91 | /** 92 | * @var string 93 | */ 94 | private $name; 95 | } 96 | -------------------------------------------------------------------------------- /src/Mock/Method/WrappedParentMethod.php: -------------------------------------------------------------------------------- 1 | callParentMethod = $callParentMethod; 32 | 33 | $this->constructWrappedMethod($method, $handle); 34 | } 35 | 36 | /** 37 | * Get the _callParent() method. 38 | * 39 | * @return ReflectionMethod The _callParent() method. 40 | */ 41 | public function callParentMethod(): ReflectionMethod 42 | { 43 | return $this->callParentMethod; 44 | } 45 | 46 | /** 47 | * Invoke this object. 48 | * 49 | * This method supports reference parameters. 50 | * 51 | * @param Arguments|array $arguments The arguments. 52 | * 53 | * @return mixed The result of invocation. 54 | * @throws Throwable If an error occurs. 55 | */ 56 | public function invokeWith($arguments = []) 57 | { 58 | if (!$arguments instanceof Arguments) { 59 | $arguments = new Arguments($arguments); 60 | } 61 | 62 | return $this->callParentMethod 63 | ->invoke($this->mock, $this->name, $arguments); 64 | } 65 | 66 | /** 67 | * @var ReflectionMethod 68 | */ 69 | private $callParentMethod; 70 | } 71 | -------------------------------------------------------------------------------- /src/Mock/Method/WrappedTraitMethod.php: -------------------------------------------------------------------------------- 1 | callTraitMethod = $callTraitMethod; 34 | $this->traitName = $traitName; 35 | 36 | $this->constructWrappedMethod($method, $handle); 37 | } 38 | 39 | /** 40 | * Get the _callTrait() method. 41 | * 42 | * @return ReflectionMethod The _callTrait() method. 43 | */ 44 | public function callTraitMethod(): ReflectionMethod 45 | { 46 | return $this->callTraitMethod; 47 | } 48 | 49 | /** 50 | * Get the trait name. 51 | * 52 | * @return string The trait name. 53 | */ 54 | public function traitName(): string 55 | { 56 | return $this->traitName; 57 | } 58 | 59 | /** 60 | * Invoke this object. 61 | * 62 | * This method supports reference parameters. 63 | * 64 | * @param Arguments|array $arguments The arguments. 65 | * 66 | * @return mixed The result of invocation. 67 | * @throws Throwable If an error occurs. 68 | */ 69 | public function invokeWith($arguments = []) 70 | { 71 | if (!$arguments instanceof Arguments) { 72 | $arguments = new Arguments($arguments); 73 | } 74 | 75 | return $this->callTraitMethod->invoke( 76 | $this->mock, 77 | $this->traitName, 78 | $this->name, 79 | $arguments 80 | ); 81 | } 82 | 83 | /** 84 | * @var ReflectionMethod 85 | */ 86 | private $callTraitMethod; 87 | 88 | /** 89 | * @var string 90 | */ 91 | private $traitName; 92 | } 93 | -------------------------------------------------------------------------------- /src/Mock/Method/WrappedUncallableMethod.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 34 | $this->returnValue = $returnValue; 35 | 36 | $this->constructWrappedMethod($method, $handle); 37 | } 38 | 39 | /** 40 | * Invoke this object. 41 | * 42 | * This method supports reference parameters. 43 | * 44 | * @param Arguments|array $arguments The arguments. 45 | * 46 | * @return mixed The result of invocation. 47 | * @throws Throwable If an error occurs. 48 | */ 49 | public function invokeWith($arguments = []) 50 | { 51 | if ($this->exception) { 52 | throw $this->exception; 53 | } 54 | 55 | return $this->returnValue; 56 | } 57 | 58 | /** 59 | * @var ?Throwable 60 | */ 61 | private $exception; 62 | 63 | /** 64 | * @var mixed 65 | */ 66 | private $returnValue; 67 | } 68 | -------------------------------------------------------------------------------- /src/Mock/Mock.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public $definitions = []; 18 | } 19 | -------------------------------------------------------------------------------- /src/Phony.php: -------------------------------------------------------------------------------- 1 | feature = $feature; 22 | 23 | parent::__construct( 24 | sprintf('Undefined feature %s.', var_export($feature, true)) 25 | ); 26 | } 27 | 28 | /** 29 | * Get the feature. 30 | * 31 | * @return string The feature. 32 | */ 33 | public function feature(): string 34 | { 35 | return $this->feature; 36 | } 37 | 38 | /** 39 | * @var string 40 | */ 41 | private $feature; 42 | } 43 | -------------------------------------------------------------------------------- /src/Sequencer/Sequencer.php: -------------------------------------------------------------------------------- 1 | current = $current; 36 | } 37 | 38 | /** 39 | * Reset the sequence number to its initial value. 40 | */ 41 | public function reset(): void 42 | { 43 | $this->current = -1; 44 | } 45 | 46 | /** 47 | * Get the sequence number. 48 | * 49 | * @return int The sequence number. 50 | */ 51 | public function get(): int 52 | { 53 | return $this->current; 54 | } 55 | 56 | /** 57 | * Increment and return the sequence number. 58 | * 59 | * @return int The sequence number. 60 | */ 61 | public function next(): int 62 | { 63 | return ++$this->current; 64 | } 65 | 66 | /** 67 | * @var array 68 | */ 69 | private static $instances = []; 70 | 71 | /** 72 | * @var int 73 | */ 74 | private $current = -1; 75 | } 76 | -------------------------------------------------------------------------------- /src/Spy/ArraySpy.php: -------------------------------------------------------------------------------- 1 | $array The array. 20 | * @param CallEventFactory $callEventFactory The call event factory to use. 21 | */ 22 | public function __construct( 23 | Call $call, 24 | array $array, 25 | CallEventFactory $callEventFactory 26 | ) { 27 | $this->call = $call; 28 | $this->array = $array; 29 | $this->callEventFactory = $callEventFactory; 30 | $this->isUsed = false; 31 | $this->isConsumed = false; 32 | } 33 | 34 | /** 35 | * Get the original iterable value. 36 | * 37 | * @return iterable The original value. 38 | */ 39 | public function iterable(): iterable 40 | { 41 | return $this->array; 42 | } 43 | 44 | /** 45 | * Get the current key. 46 | * 47 | * @return mixed The current key. 48 | */ 49 | public function key(): mixed 50 | { 51 | return key($this->array); 52 | } 53 | 54 | /** 55 | * Get the current value. 56 | * 57 | * @return mixed The current value. 58 | */ 59 | public function current(): mixed 60 | { 61 | return current($this->array); 62 | } 63 | 64 | /** 65 | * Move the current position to the next element. 66 | */ 67 | public function next(): void 68 | { 69 | next($this->array); 70 | } 71 | 72 | /** 73 | * Rewind the iterator. 74 | */ 75 | public function rewind(): void 76 | { 77 | reset($this->array); 78 | } 79 | 80 | /** 81 | * Returns true if the current iterator position is valid. 82 | * 83 | * @return bool True if the current iterator position is valid. 84 | */ 85 | public function valid(): bool 86 | { 87 | if (!$this->isUsed) { 88 | $this->call 89 | ->addIterableEvent($this->callEventFactory->createUsed()); 90 | $this->isUsed = true; 91 | } 92 | 93 | $key = key($this->array); 94 | $isValid = null !== $key; 95 | 96 | if ($this->isConsumed) { 97 | return $isValid; 98 | } 99 | 100 | if ($isValid) { 101 | $this->call->addIterableEvent( 102 | $this->callEventFactory 103 | ->createProduced($key, current($this->array)) 104 | ); 105 | } else { 106 | $this->call->setEndEvent($this->callEventFactory->createConsumed()); 107 | $this->isConsumed = true; 108 | } 109 | 110 | return $isValid; 111 | } 112 | 113 | /** 114 | * Check if a key exists. 115 | * 116 | * @param mixed $key The key. 117 | * 118 | * @return bool True if the key exists. 119 | */ 120 | public function offsetExists($key): bool 121 | { 122 | return isset($this->array[$key]); 123 | } 124 | 125 | /** 126 | * Get a value. 127 | * 128 | * @param mixed $key The key. 129 | * 130 | * @return mixed The value. 131 | */ 132 | public function offsetGet($key): mixed 133 | { 134 | return $this->array[$key]; 135 | } 136 | 137 | /** 138 | * Set a value. 139 | * 140 | * @param mixed $key The key. 141 | * @param mixed $value The value. 142 | */ 143 | public function offsetSet($key, $value): void 144 | { 145 | $this->array[$key] = $value; 146 | } 147 | 148 | /** 149 | * Un-set a value. 150 | * 151 | * @param mixed $key The key. 152 | */ 153 | public function offsetUnset($key): void 154 | { 155 | unset($this->array[$key]); 156 | } 157 | 158 | /** 159 | * Get the count. 160 | * 161 | * @return int The count. 162 | */ 163 | public function count(): int 164 | { 165 | return count($this->array); 166 | } 167 | 168 | /** 169 | * @var Call 170 | */ 171 | private $call; 172 | 173 | /** 174 | * @var array 175 | */ 176 | private $array; 177 | 178 | /** 179 | * @var CallEventFactory 180 | */ 181 | private $callEventFactory; 182 | 183 | /** 184 | * @var bool 185 | */ 186 | private $isUsed; 187 | 188 | /** 189 | * @var bool 190 | */ 191 | private $isConsumed; 192 | } 193 | -------------------------------------------------------------------------------- /src/Spy/Exception/NonArrayAccessTraversableException.php: -------------------------------------------------------------------------------- 1 | $traversable The traversable. 19 | */ 20 | public function __construct(Traversable $traversable) 21 | { 22 | $this->traversable = $traversable; 23 | 24 | parent::__construct( 25 | sprintf( 26 | 'Unable to use array access on a traversable object of type ' . 27 | '%s, since it does not implement ArrayAccess.', 28 | var_export(get_class($traversable), true) 29 | ) 30 | ); 31 | } 32 | 33 | /** 34 | * Get the traversable. 35 | * 36 | * @return Traversable The traversable. 37 | */ 38 | public function traversable(): Traversable 39 | { 40 | return $this->traversable; 41 | } 42 | 43 | /** 44 | * @var Traversable 45 | */ 46 | private $traversable; 47 | } 48 | -------------------------------------------------------------------------------- /src/Spy/Exception/NonCountableTraversableException.php: -------------------------------------------------------------------------------- 1 | $traversable The traversable. 19 | */ 20 | public function __construct(Traversable $traversable) 21 | { 22 | $this->traversable = $traversable; 23 | 24 | parent::__construct( 25 | sprintf( 26 | 'Unable to count a traversable object of type %s, since it ' . 27 | 'does not implement Countable.', 28 | var_export(get_class($traversable), true) 29 | ) 30 | ); 31 | } 32 | 33 | /** 34 | * Get the traversable. 35 | * 36 | * @return Traversable The traversable. 37 | */ 38 | public function traversable(): Traversable 39 | { 40 | return $this->traversable; 41 | } 42 | 43 | /** 44 | * @var Traversable 45 | */ 46 | private $traversable; 47 | } 48 | -------------------------------------------------------------------------------- /src/Spy/GeneratorSpyFactory.php: -------------------------------------------------------------------------------- 1 | callEventFactory = $callEventFactory; 28 | $this->generatorSpyMap = $generatorSpyMap; 29 | } 30 | 31 | /** 32 | * Create a new generator spy. 33 | * 34 | * @param Call $call The call from which the generator originated. 35 | * @param Generator $generator The generator. 36 | * 37 | * @return Generator The newly created generator spy. 38 | */ 39 | public function create(Call $call, Generator $generator): Generator 40 | { 41 | $spy = $this->createSpy($call, $generator); 42 | $this->generatorSpyMap->set($spy, $generator); 43 | 44 | return $spy; 45 | } 46 | 47 | /** 48 | * @param Generator $generator 49 | * 50 | * @return Generator 51 | */ 52 | private function createSpy(Call $call, Generator $generator): Generator 53 | { 54 | $call->addIterableEvent($this->callEventFactory->createUsed()); 55 | 56 | $isFirst = true; 57 | $received = null; 58 | $receivedException = null; 59 | 60 | while (true) { 61 | $thrown = null; 62 | 63 | try { 64 | if (!$isFirst) { 65 | if ($receivedException) { 66 | $generator->throw($receivedException); 67 | } else { 68 | $generator->send($received); 69 | } 70 | } 71 | 72 | if (!$generator->valid()) { 73 | $returnValue = $generator->getReturn(); 74 | $call->setEndEvent( 75 | $this->callEventFactory->createReturned($returnValue) 76 | ); 77 | 78 | return $returnValue; 79 | } 80 | } catch (Throwable $thrown) { 81 | $call->setEndEvent( 82 | $this->callEventFactory->createThrew($thrown) 83 | ); 84 | 85 | throw $thrown; 86 | } 87 | 88 | $key = $generator->key(); 89 | $value = $generator->current(); 90 | $received = null; 91 | $receivedException = null; 92 | 93 | $call->addIterableEvent( 94 | $this->callEventFactory->createProduced($key, $value) 95 | ); 96 | 97 | try { 98 | $received = yield $key => $value; 99 | 100 | $call->addIterableEvent( 101 | $this->callEventFactory->createReceived($received) 102 | ); 103 | } catch (Throwable $receivedException) { 104 | $call->addIterableEvent( 105 | $this->callEventFactory 106 | ->createReceivedException($receivedException) 107 | ); 108 | } 109 | 110 | $isFirst = false; 111 | unset($value); 112 | } 113 | } 114 | 115 | /** 116 | * @var CallEventFactory 117 | */ 118 | private $callEventFactory; 119 | 120 | /** 121 | * @var GeneratorSpyMap 122 | */ 123 | private $generatorSpyMap; 124 | } 125 | -------------------------------------------------------------------------------- /src/Spy/GeneratorSpyMap.php: -------------------------------------------------------------------------------- 1 | mapping = new WeakMap(); 21 | } 22 | 23 | /** 24 | * Associate a generator spy with the generator it spies on. 25 | * 26 | * @param Generator $spy The generator spy. 27 | * @param Generator $generator The generator. 28 | */ 29 | public function set(Generator $spy, Generator $generator): void 30 | { 31 | $this->mapping->offsetSet($spy, $generator); 32 | } 33 | 34 | /** 35 | * Return the generator being spied on by the supplied generator spy. 36 | * 37 | * @param Generator $spy The generator to check. 38 | * 39 | * @return ?Generator The generator, or null if the supplied generator is not a spy. 40 | */ 41 | public function get(Generator $spy): ?Generator 42 | { 43 | if ($this->mapping->offsetExists($spy)) { 44 | return $this->mapping->offsetGet($spy); 45 | } 46 | 47 | return null; 48 | } 49 | 50 | /** 51 | * @var WeakMap,Generator> 52 | */ 53 | private $mapping; 54 | } 55 | -------------------------------------------------------------------------------- /src/Spy/IterableSpy.php: -------------------------------------------------------------------------------- 1 | 15 | * @extends Iterator 16 | */ 17 | interface IterableSpy extends ArrayAccess, Countable, Iterator 18 | { 19 | /** 20 | * Get the original iterable value. 21 | * 22 | * @return iterable The original value. 23 | */ 24 | public function iterable(): iterable; 25 | } 26 | -------------------------------------------------------------------------------- /src/Spy/IterableSpyFactory.php: -------------------------------------------------------------------------------- 1 | callEventFactory = $callEventFactory; 25 | } 26 | 27 | /** 28 | * Create a new iterable spy. 29 | * 30 | * @param Call $call The call from which the iterable originated. 31 | * @param mixed $iterable The iterable. 32 | * 33 | * @return IterableSpy The newly created iterable spy. 34 | * @throws InvalidArgumentException If the supplied iterable is invalid. 35 | */ 36 | public function create(Call $call, $iterable): IterableSpy 37 | { 38 | if ($iterable instanceof Traversable) { 39 | return new TraversableSpy( 40 | $call, 41 | $iterable, 42 | $this->callEventFactory 43 | ); 44 | } 45 | 46 | if (is_array($iterable)) { 47 | return new ArraySpy($call, $iterable, $this->callEventFactory); 48 | } 49 | 50 | if (is_object($iterable)) { 51 | $type = var_export(get_class($iterable), true); 52 | } else { 53 | $type = gettype($iterable); 54 | } 55 | 56 | throw new InvalidArgumentException( 57 | sprintf('Unsupported iterable of type %s.', $type) 58 | ); 59 | } 60 | 61 | /** 62 | * @var CallEventFactory 63 | */ 64 | private $callEventFactory; 65 | } 66 | -------------------------------------------------------------------------------- /src/Spy/Spy.php: -------------------------------------------------------------------------------- 1 | $calls The calls. 66 | */ 67 | public function setCalls(array $calls): void; 68 | 69 | /** 70 | * Add a call. 71 | * 72 | * @param Call $call The call. 73 | */ 74 | public function addCall(Call $call): void; 75 | } 76 | -------------------------------------------------------------------------------- /src/Spy/SpyFactory.php: -------------------------------------------------------------------------------- 1 | labelSequencer = $labelSequencer; 33 | $this->callFactory = $callFactory; 34 | $this->invoker = $invoker; 35 | $this->generatorSpyFactory = $generatorSpyFactory; 36 | $this->iterableSpyFactory = $iterableSpyFactory; 37 | } 38 | 39 | /** 40 | * Create a new spy. 41 | * 42 | * @param ?callable $callback The callback, or null to create an anonymous spy. 43 | * 44 | * @return Spy The newly created spy. 45 | */ 46 | public function create(?callable $callback): Spy 47 | { 48 | return new SpyData( 49 | $callback, 50 | strval($this->labelSequencer->next()), 51 | $this->callFactory, 52 | $this->invoker, 53 | $this->generatorSpyFactory, 54 | $this->iterableSpyFactory 55 | ); 56 | } 57 | 58 | /** 59 | * @var Sequencer 60 | */ 61 | private $labelSequencer; 62 | 63 | /** 64 | * @var CallFactory 65 | */ 66 | private $callFactory; 67 | 68 | /** 69 | * @var Invoker 70 | */ 71 | private $invoker; 72 | 73 | /** 74 | * @var GeneratorSpyFactory 75 | */ 76 | private $generatorSpyFactory; 77 | 78 | /** 79 | * @var IterableSpyFactory 80 | */ 81 | private $iterableSpyFactory; 82 | } 83 | -------------------------------------------------------------------------------- /src/Stub/Answer/Answer.php: -------------------------------------------------------------------------------- 1 | $secondaryRequests The secondary requests. 17 | */ 18 | public function __construct( 19 | CallRequest $primaryRequest, 20 | array $secondaryRequests 21 | ) { 22 | $this->primaryRequest = $primaryRequest; 23 | $this->secondaryRequests = $secondaryRequests; 24 | } 25 | 26 | /** 27 | * Get the primary request. 28 | * 29 | * @return CallRequest The primary request. 30 | */ 31 | public function primaryRequest(): CallRequest 32 | { 33 | return $this->primaryRequest; 34 | } 35 | 36 | /** 37 | * Get the secondary requests. 38 | * 39 | * @return array The secondary requests. 40 | */ 41 | public function secondaryRequests(): array 42 | { 43 | return $this->secondaryRequests; 44 | } 45 | 46 | /** 47 | * @var CallRequest 48 | */ 49 | private $primaryRequest; 50 | 51 | /** 52 | * @var array 53 | */ 54 | private $secondaryRequests; 55 | } 56 | -------------------------------------------------------------------------------- /src/Stub/Answer/Builder/GeneratorAnswerBuilderFactory.php: -------------------------------------------------------------------------------- 1 | invocableInspector = $invocableInspector; 27 | $this->invoker = $invoker; 28 | } 29 | 30 | /** 31 | * Create a generator answer builder for the supplied stub. 32 | * 33 | * @param Stub $stub The stub. 34 | * 35 | * @return GeneratorAnswerBuilder The newly created builder. 36 | */ 37 | public function create(Stub $stub): GeneratorAnswerBuilder 38 | { 39 | return new GeneratorAnswerBuilder( 40 | $stub, 41 | $this->invocableInspector, 42 | $this->invoker 43 | ); 44 | } 45 | 46 | /** 47 | * @var InvocableInspector 48 | */ 49 | private $invocableInspector; 50 | 51 | /** 52 | * @var Invoker 53 | */ 54 | private $invoker; 55 | } 56 | -------------------------------------------------------------------------------- /src/Stub/Answer/Builder/GeneratorYieldFromIteration.php: -------------------------------------------------------------------------------- 1 | $requests The requests. 18 | * @param iterable $values The set of keys and values to yield. 19 | */ 20 | public function __construct(array $requests, $values) 21 | { 22 | $this->requests = $requests; 23 | $this->values = $values; 24 | } 25 | 26 | /** 27 | * @var array 28 | */ 29 | public $requests; 30 | 31 | /** 32 | * @var iterable 33 | */ 34 | public $values; 35 | } 36 | -------------------------------------------------------------------------------- /src/Stub/Answer/Builder/GeneratorYieldIteration.php: -------------------------------------------------------------------------------- 1 | $requests The requests. 18 | * @param bool $hasKey True if the key should be yielded. 19 | * @param mixed $key The key. 20 | * @param bool $hasValue True if the value should be yielded. 21 | * @param mixed $value The value. 22 | */ 23 | public function __construct( 24 | array $requests, 25 | bool $hasKey, 26 | $key, 27 | bool $hasValue, 28 | $value 29 | ) { 30 | $this->requests = $requests; 31 | $this->hasKey = $hasKey; 32 | $this->key = $key; 33 | $this->hasValue = $hasValue; 34 | $this->value = $value; 35 | } 36 | 37 | /** 38 | * @var array 39 | */ 40 | public $requests; 41 | 42 | /** 43 | * @var bool 44 | */ 45 | public $hasKey; 46 | 47 | /** 48 | * @var mixed 49 | */ 50 | public $key; 51 | 52 | /** 53 | * @var bool 54 | */ 55 | public $hasValue; 56 | 57 | /** 58 | * @var mixed 59 | */ 60 | public $value; 61 | } 62 | -------------------------------------------------------------------------------- /src/Stub/Answer/CallRequest.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 32 | $this->arguments = $arguments; 33 | $this->prefixSelf = $prefixSelf; 34 | $this->suffixArgumentsObject = $suffixArgumentsObject; 35 | $this->suffixArguments = $suffixArguments; 36 | 37 | foreach ($this->arguments->all() as $index => $argument) { 38 | if ($argument instanceof InstanceHandle) { 39 | $this->arguments->set($index, $argument->get()); 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * Get the callback. 46 | * 47 | * @return callable The callback. 48 | */ 49 | public function callback(): callable 50 | { 51 | return $this->callback; 52 | } 53 | 54 | /** 55 | * Get the final arguments. 56 | * 57 | * @param mixed $self The self value. 58 | * @param Arguments $arguments The incoming arguments. 59 | * 60 | * @return Arguments The final arguments. 61 | */ 62 | public function finalArguments( 63 | mixed $self, 64 | Arguments $arguments 65 | ): Arguments { 66 | $finalArguments = $this->arguments->all(); 67 | 68 | if ($this->prefixSelf) { 69 | array_unshift($finalArguments, $self); 70 | } 71 | if ($this->suffixArgumentsObject) { 72 | $finalArguments[] = $arguments; 73 | } 74 | if ($this->suffixArguments) { 75 | $finalArguments = array_merge($finalArguments, $arguments->all()); 76 | } 77 | 78 | return new Arguments($finalArguments); 79 | } 80 | 81 | /** 82 | * Get the hard-coded arguments. 83 | * 84 | * @return Arguments The hard-coded arguments. 85 | */ 86 | public function arguments(): Arguments 87 | { 88 | return $this->arguments; 89 | } 90 | 91 | /** 92 | * Returns true if the self value should be prefixed to the final arguments. 93 | * 94 | * @return bool True if the self value should be prefixed. 95 | */ 96 | public function prefixSelf(): bool 97 | { 98 | return $this->prefixSelf; 99 | } 100 | 101 | /** 102 | * Returns true if the incoming arguments should be appended to the final 103 | * arguments as an object. 104 | * 105 | * @return bool True if arguments object should be appended. 106 | */ 107 | public function suffixArgumentsObject(): bool 108 | { 109 | return $this->suffixArgumentsObject; 110 | } 111 | 112 | /** 113 | * Returns true if the incoming arguments should be appended to the final 114 | * arguments. 115 | * 116 | * @return bool True if arguments should be appended. 117 | */ 118 | public function suffixArguments(): bool 119 | { 120 | return $this->suffixArguments; 121 | } 122 | 123 | /** 124 | * @var callable 125 | */ 126 | private $callback; 127 | 128 | /** 129 | * @var Arguments 130 | */ 131 | private $arguments; 132 | 133 | /** 134 | * @var bool 135 | */ 136 | private $prefixSelf; 137 | 138 | /** 139 | * @var bool 140 | */ 141 | private $suffixArgumentsObject; 142 | 143 | /** 144 | * @var bool 145 | */ 146 | private $suffixArguments; 147 | } 148 | -------------------------------------------------------------------------------- /src/Stub/Exception/FinalReturnTypeException.php: -------------------------------------------------------------------------------- 1 | $criteria The criteria. 20 | * @param AssertionRenderer $assertionRenderer The assertion renderer to use. 21 | */ 22 | public function __construct(array $criteria, AssertionRenderer $assertionRenderer) 23 | { 24 | $this->criteria = $criteria; 25 | 26 | parent::__construct( 27 | sprintf( 28 | 'Stub criteria %s were never used. ' . 29 | 'Check for incomplete stub rules.', 30 | var_export( 31 | $assertionRenderer->renderMatchers($criteria), 32 | true 33 | ) 34 | ) 35 | ); 36 | } 37 | 38 | /** 39 | * Get the criteria. 40 | * 41 | * @return array The criteria. 42 | */ 43 | public function criteria(): array 44 | { 45 | return $this->criteria; 46 | } 47 | 48 | /** 49 | * @var array 50 | */ 51 | private $criteria; 52 | } 53 | -------------------------------------------------------------------------------- /src/Stub/StubFactory.php: -------------------------------------------------------------------------------- 1 | labelSequencer = $labelSequencer; 46 | $this->matcherFactory = $matcherFactory; 47 | $this->matcherVerifier = $matcherVerifier; 48 | $this->invoker = $invoker; 49 | $this->invocableInspector = $invocableInspector; 50 | $this->emptyValueFactory = $emptyValueFactory; 51 | $this->generatorAnswerBuilderFactory = $generatorAnswerBuilderFactory; 52 | $this->exporter = $exporter; 53 | $this->assertionRenderer = $assertionRenderer; 54 | } 55 | 56 | /** 57 | * Create a new stub. 58 | * 59 | * @param ?callable $callback The callback, or null to create an anonymous stub. 60 | * @param ?callable $defaultAnswerCallback The callback to use when creating a default answer. 61 | * 62 | * @return Stub The newly created stub. 63 | */ 64 | public function create( 65 | ?callable $callback, 66 | ?callable $defaultAnswerCallback 67 | ): Stub { 68 | if (null === $defaultAnswerCallback) { 69 | $defaultAnswerCallback = 70 | [StubData::class, 'returnsEmptyAnswerCallback']; 71 | } 72 | 73 | return new StubData( 74 | $callback, 75 | strval($this->labelSequencer->next()), 76 | $defaultAnswerCallback, 77 | $this->matcherFactory, 78 | $this->matcherVerifier, 79 | $this->invoker, 80 | $this->invocableInspector, 81 | $this->emptyValueFactory, 82 | $this->generatorAnswerBuilderFactory, 83 | $this->exporter, 84 | $this->assertionRenderer 85 | ); 86 | } 87 | 88 | /** 89 | * @var Sequencer 90 | */ 91 | private $labelSequencer; 92 | 93 | /** 94 | * @var MatcherFactory 95 | */ 96 | private $matcherFactory; 97 | 98 | /** 99 | * @var MatcherVerifier 100 | */ 101 | private $matcherVerifier; 102 | 103 | /** 104 | * @var Invoker 105 | */ 106 | private $invoker; 107 | 108 | /** 109 | * @var InvocableInspector 110 | */ 111 | private $invocableInspector; 112 | 113 | /** 114 | * @var EmptyValueFactory 115 | */ 116 | private $emptyValueFactory; 117 | 118 | /** 119 | * @var GeneratorAnswerBuilderFactory 120 | */ 121 | private $generatorAnswerBuilderFactory; 122 | 123 | /** 124 | * @var Exporter 125 | */ 126 | private $exporter; 127 | 128 | /** 129 | * @var AssertionRenderer 130 | */ 131 | private $assertionRenderer; 132 | } 133 | -------------------------------------------------------------------------------- /src/Stub/StubRule.php: -------------------------------------------------------------------------------- 1 | $criteria The criteria. 20 | * @param array $answers The answers. 21 | */ 22 | public function __construct(array $criteria, array $answers) 23 | { 24 | $this->criteria = $criteria; 25 | $this->answers = $answers; 26 | 27 | $this->lastIndex = count($answers) - 1; 28 | $this->calledCount = 0; 29 | } 30 | 31 | /** 32 | * Get the criteria. 33 | * 34 | * @return array The criteria. 35 | */ 36 | public function criteria(): array 37 | { 38 | return $this->criteria; 39 | } 40 | 41 | /** 42 | * Get the answers. 43 | * 44 | * @return array The answers. 45 | */ 46 | public function answers(): array 47 | { 48 | return $this->answers; 49 | } 50 | 51 | /** 52 | * Get the next answer. 53 | * 54 | * @return Answer The answer. 55 | * @throws UndefinedAnswerException If an undefined or incomplete answer is encountered. 56 | */ 57 | public function next(): Answer 58 | { 59 | if ($this->calledCount > $this->lastIndex) { 60 | $index = $this->lastIndex; 61 | } else { 62 | $index = $this->calledCount; 63 | } 64 | 65 | ++$this->calledCount; 66 | 67 | if (!isset($this->answers[$index])) { 68 | throw new UndefinedAnswerException(); 69 | } 70 | 71 | return $this->answers[$index]; 72 | } 73 | 74 | /** 75 | * @var array 76 | */ 77 | private $criteria; 78 | 79 | /** 80 | * @var array 81 | */ 82 | private $answers; 83 | 84 | /** 85 | * @var int 86 | */ 87 | private $lastIndex; 88 | 89 | /** 90 | * @var int 91 | */ 92 | private $calledCount; 93 | } 94 | -------------------------------------------------------------------------------- /src/Verification/Cardinality.php: -------------------------------------------------------------------------------- 1 | = 0 && $minimum > $maximum) { 37 | throw new InvalidCardinalityStateException(); 38 | } 39 | 40 | if ($maximum < 0 && !$minimum) { 41 | throw new InvalidCardinalityStateException(); 42 | } 43 | 44 | $this->minimum = $minimum; 45 | $this->maximum = $maximum; 46 | $this->setIsAlways($isAlways); 47 | } 48 | 49 | /** 50 | * Get the minimum. 51 | * 52 | * @return int The minimum. 53 | */ 54 | public function minimum(): int 55 | { 56 | return $this->minimum; 57 | } 58 | 59 | /** 60 | * Get the maximum. 61 | * 62 | * @return int The maximum. 63 | */ 64 | public function maximum(): int 65 | { 66 | return $this->maximum; 67 | } 68 | 69 | /** 70 | * Returns true if this cardinality is 'never'. 71 | * 72 | * @return bool True if this cardinality is 'never'. 73 | */ 74 | public function isNever(): bool 75 | { 76 | return 0 === $this->maximum; 77 | } 78 | 79 | /** 80 | * Turn 'always' on or off. 81 | * 82 | * @param bool $isAlways True to enable 'always'. 83 | * 84 | * @throws InvalidCardinalityException If the cardinality is invalid. 85 | */ 86 | public function setIsAlways(bool $isAlways): void 87 | { 88 | if ($isAlways && $this->isNever()) { 89 | throw new InvalidCardinalityStateException(); 90 | } 91 | 92 | $this->isAlways = $isAlways; 93 | } 94 | 95 | /** 96 | * Returns true if 'always' is enabled. 97 | * 98 | * @return bool True if 'always' is enabled. 99 | */ 100 | public function isAlways(): bool 101 | { 102 | return $this->isAlways; 103 | } 104 | 105 | /** 106 | * Returns true if the supplied count matches this cardinality. 107 | * 108 | * @param int|bool $count The count or result to check. 109 | * @param int $maximumCount The maximum possible count. 110 | * 111 | * @return bool True if the supplied count matches this cardinality. 112 | */ 113 | public function matches($count, int $maximumCount): bool 114 | { 115 | $count = intval($count); 116 | $result = true; 117 | 118 | if ($count < $this->minimum) { 119 | $result = false; 120 | } 121 | 122 | if ($this->maximum >= 0 && $count > $this->maximum) { 123 | $result = false; 124 | } 125 | 126 | if ($this->isAlways && $count < $maximumCount) { 127 | $result = false; 128 | } 129 | 130 | return $result; 131 | } 132 | 133 | /** 134 | * Asserts that this cardinality is suitable for events that can only happen 135 | * once or not at all. 136 | * 137 | * @return $this This cardinality. 138 | * @throws InvalidCardinalityException If the cardinality is invalid. 139 | */ 140 | public function assertSingular(): self 141 | { 142 | if ($this->minimum > 1 || $this->maximum > 1 || $this->isAlways) { 143 | throw new InvalidSingularCardinalityException($this); 144 | } 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * @var int 151 | */ 152 | private $minimum; 153 | 154 | /** 155 | * @var int 156 | */ 157 | private $maximum; 158 | 159 | /** 160 | * @var bool 161 | */ 162 | private $isAlways; 163 | } 164 | -------------------------------------------------------------------------------- /src/Verification/CardinalityVerifier.php: -------------------------------------------------------------------------------- 1 | cardinality = new Cardinality(0, 0); 22 | 23 | return $this; 24 | } 25 | 26 | /** 27 | * Requires that the next verification matches only once. 28 | * 29 | * @return $this This verifier. 30 | */ 31 | public function once(): CardinalityVerifier 32 | { 33 | $this->cardinality = new Cardinality(1, 1); 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * Requires that the next verification matches exactly two times. 40 | * 41 | * @return $this This verifier. 42 | */ 43 | public function twice(): CardinalityVerifier 44 | { 45 | $this->cardinality = new Cardinality(2, 2); 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * Requires that the next verification matches exactly three times. 52 | * 53 | * @return $this This verifier. 54 | */ 55 | public function thrice(): CardinalityVerifier 56 | { 57 | $this->cardinality = new Cardinality(3, 3); 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Requires that the next verification matches an exact number of times. 64 | * 65 | * @param int $times The match count. 66 | * 67 | * @return $this This verifier. 68 | */ 69 | public function times(int $times): CardinalityVerifier 70 | { 71 | $this->cardinality = new Cardinality($times, $times); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Requires that the next verification matches a number of times greater 78 | * than or equal to $minimum. 79 | * 80 | * @param int $minimum The minimum match count. 81 | * 82 | * @return $this This verifier. 83 | */ 84 | public function atLeast(int $minimum): CardinalityVerifier 85 | { 86 | $this->cardinality = new Cardinality($minimum, -1); 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * Requires that the next verification matches a number of times less than 93 | * or equal to $maximum. 94 | * 95 | * @param int $maximum The maximum match count. 96 | * 97 | * @return $this This verifier. 98 | */ 99 | public function atMost(int $maximum): CardinalityVerifier 100 | { 101 | $this->cardinality = new Cardinality(0, $maximum); 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Requires that the next verification matches a number of times greater 108 | * than or equal to $minimum, and less than or equal to $maximum. 109 | * 110 | * @param int $minimum The minimum match count. 111 | * @param int $maximum The maximum match count. 112 | * 113 | * @return $this This verifier. 114 | * @throws InvalidCardinalityException If the cardinality is invalid. 115 | */ 116 | public function between(int $minimum, int $maximum): CardinalityVerifier 117 | { 118 | $this->cardinality = new Cardinality($minimum, $maximum); 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * Requires that the next verification matches for all possible items. 125 | * 126 | * @return $this This verifier. 127 | */ 128 | public function always(): CardinalityVerifier 129 | { 130 | $this->cardinality->setIsAlways(true); 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * Reset the cardinality to its default value. 137 | * 138 | * @return Cardinality The current cardinality. 139 | */ 140 | public function resetCardinality(): Cardinality 141 | { 142 | $cardinality = $this->cardinality; 143 | $this->cardinality = new Cardinality(); 144 | 145 | return $cardinality; 146 | } 147 | 148 | /** 149 | * Get the cardinality. 150 | * 151 | * @return Cardinality The cardinality. 152 | */ 153 | public function cardinality(): Cardinality 154 | { 155 | return $this->cardinality; 156 | } 157 | 158 | /** 159 | * @var Cardinality 160 | */ 161 | protected $cardinality; 162 | } 163 | -------------------------------------------------------------------------------- /src/Verification/Exception/InvalidCardinalityException.php: -------------------------------------------------------------------------------- 1 | cardinality = $cardinality; 25 | 26 | parent::__construct( 27 | 'The specified cardinality is invalid for events ' . 28 | 'that can only happen once or not at all.' 29 | ); 30 | } 31 | 32 | /** 33 | * Get the cardinality. 34 | * 35 | * @return Cardinality The cardinality. 36 | */ 37 | public function cardinality(): Cardinality 38 | { 39 | return $this->cardinality; 40 | } 41 | 42 | /** 43 | * @var Cardinality 44 | */ 45 | private $cardinality; 46 | } 47 | -------------------------------------------------------------------------------- /src/Verification/GeneratorVerifierFactory.php: -------------------------------------------------------------------------------- 1 | matcherFactory = $matcherFactory; 32 | $this->assertionRecorder = $assertionRecorder; 33 | $this->assertionRenderer = $assertionRenderer; 34 | } 35 | 36 | /** 37 | * Set the call verifier factory. 38 | * 39 | * @param CallVerifierFactory $callVerifierFactory The call verifier factory to use. 40 | */ 41 | public function setCallVerifierFactory( 42 | CallVerifierFactory $callVerifierFactory 43 | ): void { 44 | $this->callVerifierFactory = $callVerifierFactory; 45 | } 46 | 47 | /** 48 | * Create a new generator verifier. 49 | * 50 | * @param Spy|Call $subject The subject. 51 | * @param array $calls The calls. 52 | * 53 | * @return GeneratorVerifier The newly created generator verifier. 54 | */ 55 | public function create($subject, array $calls): GeneratorVerifier 56 | { 57 | return new GeneratorVerifier( 58 | $subject, 59 | $calls, 60 | $this->matcherFactory, 61 | $this->callVerifierFactory, 62 | $this->assertionRecorder, 63 | $this->assertionRenderer 64 | ); 65 | } 66 | 67 | /** 68 | * @var MatcherFactory 69 | */ 70 | private $matcherFactory; 71 | 72 | /** 73 | * @var AssertionRecorder 74 | */ 75 | private $assertionRecorder; 76 | 77 | /** 78 | * @var AssertionRenderer 79 | */ 80 | private $assertionRenderer; 81 | 82 | /** 83 | * @var CallVerifierFactory 84 | */ 85 | private $callVerifierFactory; 86 | } 87 | -------------------------------------------------------------------------------- /src/Verification/IterableVerifierFactory.php: -------------------------------------------------------------------------------- 1 | matcherFactory = $matcherFactory; 32 | $this->assertionRecorder = $assertionRecorder; 33 | $this->assertionRenderer = $assertionRenderer; 34 | } 35 | 36 | /** 37 | * Set the call verifier factory. 38 | * 39 | * @param CallVerifierFactory $callVerifierFactory The call verifier factory to use. 40 | */ 41 | public function setCallVerifierFactory( 42 | CallVerifierFactory $callVerifierFactory 43 | ): void { 44 | $this->callVerifierFactory = $callVerifierFactory; 45 | } 46 | 47 | /** 48 | * Create a new iterable verifier. 49 | * 50 | * @param Spy|Call $subject The subject. 51 | * @param array $calls The calls. 52 | * 53 | * @return IterableVerifier The newly created iterable verifier. 54 | */ 55 | public function create($subject, array $calls): IterableVerifier 56 | { 57 | return new IterableVerifier( 58 | $subject, 59 | $calls, 60 | $this->matcherFactory, 61 | $this->callVerifierFactory, 62 | $this->assertionRecorder, 63 | $this->assertionRenderer 64 | ); 65 | } 66 | 67 | /** 68 | * @var MatcherFactory 69 | */ 70 | private $matcherFactory; 71 | 72 | /** 73 | * @var AssertionRecorder 74 | */ 75 | private $assertionRecorder; 76 | 77 | /** 78 | * @var AssertionRenderer 79 | */ 80 | private $assertionRenderer; 81 | 82 | /** 83 | * @var CallVerifierFactory 84 | */ 85 | private $callVerifierFactory; 86 | } 87 | -------------------------------------------------------------------------------- /src/initialize.php: -------------------------------------------------------------------------------- 1 |