├── LICENSE.md ├── SECURITY.md ├── composer.json ├── rector.php └── src ├── Autoload.php ├── Exceptions └── InvalidDataException.php ├── Expectations ├── Authentication.php ├── Collections.php ├── Database.php ├── Exceptions.php ├── Models.php ├── Response.php ├── Storage.php ├── Time.php └── Views.php ├── Helpers ├── Models │ └── RelationshipGuesser.php └── ValueProcessor.php └── Plugin.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Pest and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you discover any security related issues, please email report@defstudio.it instead of using the issue tracker. 4 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "defstudio/pest-plugin-laravel-expectations", 3 | "description": "A plugin to add laravel tailored expectations to Pest", 4 | "keywords": [ 5 | "php", 6 | "framework", 7 | "pest", 8 | "unit", 9 | "test", 10 | "testing", 11 | "plugin", 12 | "laravel", 13 | "expectations" 14 | ], 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Fabio Ivona", 19 | "email": "fabio.ivona@defstudio.it", 20 | "homepage": "https://defstudio.it", 21 | "role": "Developer" 22 | }, 23 | { 24 | "name": "Daniele Romeo", 25 | "email": "danieleromeo@defstudio.it", 26 | "homepage": "https://defstudio.it", 27 | "role": "Developer" 28 | } 29 | ], 30 | "require": { 31 | "php": "^8.1.0", 32 | "illuminate/contracts": "^10.0|^11.0.3|^12.0", 33 | "illuminate/database": "^10.0|^11.0.3|^12.0", 34 | "illuminate/http": "^10.0|^11.0.3|^12.0", 35 | "illuminate/support": "^10.0|^11.0.3|^12.0", 36 | "illuminate/testing": "^10.0|^11.0.3|^12.0", 37 | "pestphp/pest": "^2.0|^3.0", 38 | "pestphp/pest-plugin": "^2.0|^3.0", 39 | "pestphp/pest-plugin-laravel": "^2.0|^3.0" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "DefStudio\\PestLaravelExpectations\\": "src/" 44 | }, 45 | "files": [ 46 | "src/Autoload.php" 47 | ] 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Tests\\": "tests/" 52 | } 53 | }, 54 | "require-dev": { 55 | "orchestra/testbench": "^8.0|^9.0", 56 | "nesbot/carbon": "^2.62.1|^3.0.0", 57 | "laravel/pint": "^1.11.0", 58 | "phpstan/phpstan": "^1.10.29", 59 | "phpstan/phpstan-strict-rules": "^1.5.1", 60 | "symfony/var-dumper": "^6.3.3|^v7.0.4", 61 | "symplify/phpstan-rules": "^13.0.1", 62 | "rector/rector": "^1.0.3", 63 | "thecodingmachine/phpstan-strict-rules": "^1.0.0", 64 | "ergebnis/phpstan-rules": "^2.1.0" 65 | }, 66 | "extra": { 67 | "branch-alias": { 68 | "dev-master": "2.x-dev" 69 | } 70 | }, 71 | "minimum-stability": "dev", 72 | "prefer-stable": true, 73 | "config": { 74 | "sort-packages": true, 75 | "preferred-install": "dist", 76 | "allow-plugins": { 77 | "pestphp/pest-plugin": true 78 | } 79 | }, 80 | "scripts": { 81 | "refactor": "rector", 82 | "lint": "pint", 83 | "test:refactor": "rector --dry-run", 84 | "test:lint": "pint --test", 85 | "test:types": "phpstan analyse --ansi --memory-limit=-1 --debug", 86 | "test:unit": "php vendor/bin/pest --colors=always --exclude-group=integration --compact", 87 | "update:snapshots": "REBUILD_SNAPSHOTS=true php vendor/bin/pest --colors=always", 88 | "test": [ 89 | "@test:refactor", 90 | "@test:lint", 91 | "@test:types", 92 | "@test:unit" 93 | ], 94 | "coverage": "@test:unit --coverage" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 12 | __DIR__.'/src', 13 | ]); 14 | 15 | $rectorConfig->rules([ 16 | InlineConstructorDefaultToPropertyRector::class, 17 | ]); 18 | 19 | $rectorConfig->sets([ 20 | LevelSetList::UP_TO_PHP_81, 21 | SetList::CODE_QUALITY, 22 | SetList::DEAD_CODE, 23 | SetList::EARLY_RETURN, 24 | SetList::TYPE_DECLARATION, 25 | SetList::PRIVATIZATION, 26 | ]); 27 | }; 28 | -------------------------------------------------------------------------------- /src/Autoload.php: -------------------------------------------------------------------------------- 1 | shortenedExport($value), $class)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Expectations/Authentication.php: -------------------------------------------------------------------------------- 1 | extend( 18 | 'toBeAuthenticated', 19 | /** 20 | * Assert that the given User is authenticated. 21 | */ 22 | function (?string $guard = null): Expectation { 23 | assertAuthenticated($guard); 24 | 25 | /** @var Authenticatable $authenticated */ 26 | $authenticated = Auth::guard($guard)->user(); 27 | 28 | // @phpstan-ignore-next-line 29 | assertEquals($this->value->id, $authenticated->id, "The User ID #{$this->value->id} doesn't match authenticated User ID #{$authenticated->id}"); 30 | 31 | return $this; 32 | } 33 | ); 34 | 35 | expect()->extend( 36 | 'toBeValidCredentials', 37 | /** 38 | * Assert that the given credentials are valid. 39 | */ 40 | function (?string $guard = null): Expectation { 41 | assertCredentials($this->value, $guard); 42 | 43 | return $this; 44 | } 45 | ); 46 | 47 | expect()->extend( 48 | 'toBeInvalidCredentials', 49 | /** 50 | * Assert that the given credentials are invalid. 51 | */ 52 | function (?string $guard = null): Expectation { 53 | assertInvalidCredentials($this->value, $guard); 54 | 55 | return $this; 56 | } 57 | ); 58 | 59 | expect()->extend( 60 | 'toBeAbleTo', 61 | /** 62 | * @param array|mixed $arguments 63 | */ 64 | function (string $ability, $arguments = []): Expectation { 65 | /** @var Authorizable $user */ 66 | $user = $this->value; 67 | 68 | $exporter = new Exporter; 69 | 70 | $arguments_string = $exporter->shortenedExport($arguments); 71 | assertTrue($user->can($ability, $arguments), sprintf('Failed asserting that the given user is authorized to "%s" with [%s]', $ability, $arguments_string)); 72 | 73 | return $this; 74 | } 75 | ); 76 | -------------------------------------------------------------------------------- /src/Expectations/Collections.php: -------------------------------------------------------------------------------- 1 | extend( 9 | 'toBeCollection', 10 | /** 11 | * Assert that the value is an instance of \Illuminate\Support\Collection. 12 | */ 13 | fn (): Expectation => // @phpstan-ignore-next-line 14 | $this->toBeInstanceOf(Collection::class) 15 | ); 16 | 17 | expect()->extend( 18 | 'toBeEloquentCollection', 19 | /** 20 | * Assert that the value is an instance of \Illuminate\Database\Eloquent\Collection. 21 | */ 22 | fn (): Expectation => // @phpstan-ignore-next-line 23 | $this->toBeInstanceOf(\Illuminate\Database\Eloquent\Collection::class) 24 | ); 25 | -------------------------------------------------------------------------------- /src/Expectations/Database.php: -------------------------------------------------------------------------------- 1 | extend( 14 | 'toBeInDatabase', 15 | /** 16 | * Assert that the given "where condition" exists in the database. 17 | */ 18 | function (string $table, ?string $connection = null): Expectation { 19 | assertDatabaseHas($table, $this->value, $connection); 20 | 21 | return $this; 22 | } 23 | ); 24 | 25 | expect()->extend('toMatchQuery', function (string $sql, array $bindings, bool $exact = true): void { 26 | if ($exact) { 27 | Assert::assertSame($sql, $this->value->toSql()); 28 | Assert::assertSame($bindings, $this->value->getBindings()); 29 | } else { 30 | Assert::assertStringContainsString($sql, $this->value->toSql()); 31 | 32 | $bindingsCountBefore = Str::of($this->value->toSql())->before($sql)->substrCount('?'); 33 | 34 | $queryBindings = $this->value->getBindings(); 35 | $queryBindings = array_slice($queryBindings, $bindingsCountBefore, count($bindings)); 36 | 37 | Assert::assertSame($bindings, $queryBindings); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /src/Expectations/Exceptions.php: -------------------------------------------------------------------------------- 1 | extend( 9 | 'toThrowValidationException', 10 | /** 11 | * Assert that the given expectation errors are thrown 12 | * 13 | * @phpstan-ignore-next-line 14 | */ 15 | fn (?array $errors = null): Expectation => expect($this->value)->toThrow(function (ValidationException $exception) use ($errors): void { 16 | if ($errors !== null) { 17 | expect($exception->errors())->toBe($errors); 18 | } 19 | })); 20 | -------------------------------------------------------------------------------- /src/Expectations/Models.php: -------------------------------------------------------------------------------- 1 | extend( 24 | 'toBeDeleted', 25 | /** 26 | * Assert that the given model is deleted. 27 | */ 28 | function (): Expectation { 29 | assertModelMissing($this->value); 30 | 31 | return $this; 32 | } 33 | ); 34 | 35 | expect()->extend( 36 | 'toBelongTo', 37 | /* 38 | * Asserts that the given model belongs to a parent model 39 | */ 40 | function (Model $related, string $relationshipName = ''): Expectation { 41 | /** @var Model $model */ 42 | $model = $this->value; 43 | 44 | $guesser = Models\RelationshipGuesser::from($model) 45 | ->to($related) 46 | ->ofType(BelongsTo::class) 47 | ->withHint($relationshipName); 48 | 49 | $relationshipName = $guesser->guess(); 50 | 51 | $foreignKey = $guesser->getRelationship()->getForeignKeyName(); 52 | 53 | $modelClass = $model::class; 54 | $relatedClass = $related::class; 55 | 56 | // @phpstan-ignore-next-line 57 | assertEquals($relatedClass, $guesser->getRelationship()->getModel()::class, "Failed asserting that [$modelClass#$model->id] belongs to [$relatedClass#$related->id] through its relationship '$relationshipName'"); 58 | 59 | assertEquals($model->$foreignKey, $related->id, "Failed asserting that [$modelClass#$model->id] belongs to [$relatedClass#$related->id]"); 60 | 61 | return $this; 62 | } 63 | ); 64 | 65 | expect()->extend( 66 | 'toBeSameModelAs', 67 | /** 68 | * Assert that the given model has the same ID and belong to the same table of another model. 69 | */ 70 | function (Model $model): Expectation { 71 | /** @var Model $value */ 72 | $value = $this->value; 73 | 74 | assertTrue($value->is($model), 'Failed asserting that two models have the same ID and belongs to the same table'); 75 | 76 | return $this; 77 | } 78 | ); 79 | 80 | expect()->extend( 81 | 'toBeSoftDeleted', 82 | /** 83 | * Assert that the given model is soft deleted. 84 | */ 85 | function (string $deletedAtColumn = 'deleted_at'): Expectation { 86 | assertSoftDeleted( 87 | $this->value, 88 | [], 89 | null, 90 | $deletedAtColumn 91 | ); 92 | 93 | return $this; 94 | } 95 | ); 96 | 97 | expect()->extend( 98 | 'toExist', 99 | /** 100 | * Asserts that the given model exists in the database. 101 | */ 102 | function (): Expectation { 103 | assertDatabaseHas( 104 | $this->value->getTable(), 105 | [$this->value->getKeyName() => $this->value->getKey()], 106 | $this->value->getConnectionName() 107 | ); 108 | 109 | return $this; 110 | } 111 | ); 112 | 113 | expect()->extend( 114 | 'toOwn', 115 | /** 116 | * Asserts that the given model owns child model. 117 | */ 118 | function (Model $related, string $relationshipName = ''): Expectation { 119 | /** @var Model $model */ 120 | $model = $this->value; 121 | 122 | $guesser = Models\RelationshipGuesser::from($model) 123 | ->to($related) 124 | ->ofType(HasOne::class) 125 | ->withHint($relationshipName) 126 | ->throwException(false); 127 | 128 | $relationshipName = $guesser->guess(); 129 | 130 | $relationshipName = $relationshipName ?: $guesser->ofType(HasMany::class)->throwException(true)->guess(); 131 | 132 | $foreignKey = $guesser->getRelationship()->getForeignKeyName(); 133 | 134 | $modelClass = $model::class; 135 | $relatedClass = $related::class; 136 | 137 | // @phpstan-ignore-next-line 138 | assertEquals($relatedClass, $guesser->getRelationship()->getModel()::class, "Failed asserting that [$modelClass#$model->id] has a relationship '$relationshipName' with [$relatedClass#$related->id]"); 139 | 140 | assertEquals($related->$foreignKey, $model->id, "Failed asserting that [$modelClass#$model->id] has a relationship with [$relatedClass#$related->id]"); 141 | 142 | return $this; 143 | } 144 | ); 145 | 146 | expect()->intercept('toBe', Model::class, fn (Model $anotherModel) => expect($this->value)->toBeSameModelAs($anotherModel)); // @phpstan-ignore-line 147 | -------------------------------------------------------------------------------- /src/Expectations/Response.php: -------------------------------------------------------------------------------- 1 | value; 20 | 21 | if ($response instanceof TestResponse) { 22 | return $response; 23 | } 24 | 25 | return TestResponse::fromBaseResponse($response); 26 | } 27 | 28 | expect()->extend( 29 | 'toBeRedirect', 30 | /** 31 | * Assert that the response is a redirection. 32 | */ 33 | function (?string $uri = null): Expectation { 34 | $response = getTestableResponse($this); 35 | 36 | $response->assertRedirect(); 37 | 38 | if ($uri === null) { 39 | return $this; 40 | } 41 | 42 | try { 43 | $response->assertLocation($uri); 44 | } catch (ExpectationFailedException) { 45 | throw new ExpectationFailedException("Failed asserting that the redirect uri [{$response->headers->get('Location')}] matches [$uri]"); 46 | } 47 | 48 | return $this; 49 | } 50 | ); 51 | 52 | expect()->extend( 53 | 'toBeRedirectToSignedRoute', 54 | /** 55 | * Assert whether the response is redirecting to a given signed route. 56 | */ 57 | function (?string $name = null, mixed $parameters = []): Expectation { 58 | $response = getTestableResponse($this); 59 | 60 | $response->assertRedirectToSignedRoute($name, $parameters); 61 | 62 | return $this; 63 | } 64 | ); 65 | 66 | expect()->extend( 67 | 'toBeSuccessful', 68 | /** 69 | * Assert that the response has a successful status code. 70 | */ 71 | function (): Expectation { 72 | $response = getTestableResponse($this); 73 | 74 | $response->assertSuccessful(); 75 | 76 | return $this; 77 | } 78 | ); 79 | 80 | expect()->extend( 81 | 'toBeOk', 82 | /** 83 | * Assert that the response has a 200 status code. 84 | */ 85 | function (): Expectation { 86 | $response = getTestableResponse($this); 87 | 88 | $response->assertOk(); 89 | 90 | return $this; 91 | } 92 | ); 93 | 94 | expect()->extend( 95 | 'toConfirmCreation', 96 | /** 97 | * Assert that the response has a 201 status code. 98 | */ 99 | function (): Expectation { 100 | $response = getTestableResponse($this); 101 | 102 | $response->assertCreated(); 103 | 104 | return $this; 105 | } 106 | ); 107 | 108 | expect()->extend( 109 | 'toBeNotFound', 110 | /** 111 | * Assert that the response has a not found status code. 112 | */ 113 | function (): Expectation { 114 | $response = getTestableResponse($this); 115 | 116 | $response->assertNotFound(); 117 | 118 | return $this; 119 | } 120 | ); 121 | 122 | expect()->extend( 123 | 'toBeUnauthorized', 124 | /** 125 | * Assert that the response has an unauthorized status code. 126 | */ 127 | function (): Expectation { 128 | $response = getTestableResponse($this); 129 | 130 | $response->assertUnauthorized(); 131 | 132 | return $this; 133 | } 134 | ); 135 | 136 | expect()->extend( 137 | 'toHaveNoContent', 138 | /** 139 | * Assert that the response has the given status code and no content. 140 | */ 141 | function (int $status = 204): Expectation { 142 | $response = getTestableResponse($this); 143 | 144 | $response->assertNoContent($status); 145 | 146 | return $this; 147 | } 148 | ); 149 | 150 | expect()->extend( 151 | 'toBeForbidden', 152 | /** 153 | * Assert that the response has a forbidden status code. 154 | */ 155 | function (): Expectation { 156 | $response = getTestableResponse($this); 157 | 158 | $response->assertForbidden(); 159 | 160 | return $this; 161 | } 162 | ); 163 | 164 | expect()->extend( 165 | 'toHaveStatus', 166 | /** 167 | * Assert that the response has the given status code. 168 | */ 169 | function (int $status): Expectation { 170 | $response = getTestableResponse($this); 171 | 172 | $response->assertStatus($status); 173 | 174 | return $this; 175 | } 176 | ); 177 | 178 | expect()->extend( 179 | 'toBeDownload', 180 | /** 181 | * Assert that the response offers a file download. 182 | */ 183 | function (?string $filename = null): Expectation { 184 | $response = getTestableResponse($this); 185 | 186 | try { 187 | $response->assertDownload($filename); 188 | } catch (AssertionFailedError $exception) { 189 | throw new ExpectationFailedException($exception->getMessage()); 190 | } 191 | 192 | return $this; 193 | } 194 | ); 195 | 196 | // TODO: alias with ->toContain() when the pipe PR gets merged 197 | expect()->extend( 198 | 'toRender', 199 | /** 200 | * Assert that the response contains the given string or array of strings. 201 | */ 202 | function (string|array $string, bool $escape = false): Expectation { 203 | $response = getTestableResponse($this); 204 | 205 | $response->assertSee($string, $escape); 206 | 207 | return $this; 208 | } 209 | ); 210 | 211 | // TODO: alias with ->toContainInOrder() when the pipe PR gets merged 212 | expect()->extend( 213 | 'toRenderInOrder', 214 | /** 215 | * Assert that the response contains the given ordered sequence of strings. 216 | */ 217 | function (array $strings, bool $escape = false): Expectation { 218 | $response = getTestableResponse($this); 219 | 220 | $response->assertSeeInOrder($strings, $escape); 221 | 222 | return $this; 223 | } 224 | ); 225 | 226 | expect()->extend( 227 | 'toRenderText', 228 | /** 229 | * Assert that the response contains the given string or array of strings in its text. 230 | */ 231 | function (string|array $text, bool $escape = false): Expectation { 232 | $response = getTestableResponse($this); 233 | 234 | $response->assertSeeText($text, $escape); 235 | 236 | return $this; 237 | } 238 | ); 239 | 240 | expect()->extend( 241 | 'toRenderTextInOrder', 242 | /** 243 | * Assert that the response contains the given ordered sequence of strings in its text. 244 | */ 245 | function (array $texts, bool $escape = false): Expectation { 246 | $response = getTestableResponse($this); 247 | 248 | $response->assertSeeTextInOrder($texts, $escape); 249 | 250 | return $this; 251 | } 252 | ); 253 | 254 | expect()->extend( 255 | 'toContainText', 256 | /** 257 | * Assert that the response contains the fiven string or array of strings in its text. 258 | */ 259 | fn (string|array $text, bool $escape = false): Expectation => $this->toRenderText($text, $escape) 260 | ); 261 | 262 | expect()->extend( 263 | 'toContainTextInOrder', 264 | /** 265 | * Assert that the response contains the given ordered sequence of strings in its text. 266 | */ 267 | fn (string|array $text, bool $escape = false): Expectation => $this->toRenderTextInOrder($text, $escape) 268 | ); 269 | 270 | expect()->extend( 271 | 'toHaveJson', 272 | /** 273 | * Assert that the response is a superset of the given JSON. 274 | */ 275 | function (array|callable $json, bool $strict = false): Expectation { 276 | $response = getTestableResponse($this); 277 | 278 | $response->assertJson($json, $strict); 279 | 280 | return $this; 281 | } 282 | ); 283 | 284 | expect()->extend( 285 | 'toHaveExactJson', 286 | /** 287 | * Assert that the response has the exact given JSON. 288 | */ 289 | function (array $json): Expectation { 290 | $response = getTestableResponse($this); 291 | 292 | $response->assertExactJson($json); 293 | 294 | return $this; 295 | } 296 | ); 297 | 298 | expect()->extend( 299 | 'toHaveJsonFragment', 300 | /** 301 | * Assert that the response contains the given JSON fragment. 302 | */ 303 | function (array $json): Expectation { 304 | $response = getTestableResponse($this); 305 | 306 | $response->assertJsonFragment($json); 307 | 308 | return $this; 309 | } 310 | ); 311 | 312 | expect()->extend( 313 | 'toHaveJsonStructure', 314 | /** 315 | * Assert that the response has a given JSON structure. 316 | */ 317 | function (?array $structure = null, ?array $responseData = null): Expectation { 318 | $response = getTestableResponse($this); 319 | 320 | $response->assertJsonStructure($structure, $responseData); 321 | 322 | return $this; 323 | } 324 | ); 325 | 326 | expect()->extend( 327 | 'toHaveJsonPath', 328 | /** 329 | * Assert that the expected value and type exists at the given path in the response. 330 | */ 331 | function (string $path, mixed $expect): Expectation { 332 | $response = getTestableResponse($this); 333 | 334 | $response->assertJsonPath($path, $expect); 335 | 336 | return $this; 337 | } 338 | ); 339 | 340 | expect()->extend( 341 | 'toHaveJsonValidationErrors', 342 | /** 343 | * Assert that the response has the given JSON validation errors. 344 | */ 345 | function (string|array|null $errors = null, string $responseKey = 'errors'): Expectation { 346 | $response = getTestableResponse($this); 347 | 348 | $response->assertJsonValidationErrors($errors ?? [], $responseKey); 349 | 350 | return $this; 351 | }, 352 | ); 353 | 354 | expect()->extend( 355 | 'toHaveValid', 356 | /** 357 | * Assert that the response doesn't have the given validation error keys. 358 | */ 359 | function (string|array|null $keys = null, string $errorBag = 'default', string $responseKey = 'errors'): Expectation { 360 | $response = getTestableResponse($this); 361 | 362 | $response->assertValid($keys, $errorBag, $responseKey); 363 | 364 | return $this; 365 | }, 366 | ); 367 | 368 | expect()->extend( 369 | 'toHaveInvalid', 370 | /** 371 | * Assert that the response has the given validation error keys. 372 | */ 373 | function (string|array|null $keys = null, string $errorBag = 'default', string $responseKey = 'errors'): Expectation { 374 | $response = getTestableResponse($this); 375 | 376 | $response->assertInvalid($keys, $errorBag, $responseKey); 377 | 378 | return $this; 379 | }, 380 | ); 381 | 382 | expect()->extend( 383 | 'toHaveHeader', 384 | /** 385 | * Assert that the response contains the given header and equals the optional value. 386 | */ 387 | function (string $headerName, mixed $value = null): Expectation { 388 | $response = getTestableResponse($this); 389 | 390 | $response->assertHeader($headerName, $value); 391 | 392 | return $this; 393 | } 394 | ); 395 | 396 | expect()->extend( 397 | 'toHaveMissingHeader', 398 | /** 399 | * Asserts that the response does not contain the given header. 400 | */ 401 | function (string $headerName): Expectation { 402 | $response = getTestableResponse($this); 403 | 404 | $response->assertHeaderMissing($headerName); 405 | 406 | return $this; 407 | } 408 | ); 409 | 410 | expect()->extend( 411 | 'toHaveSession', 412 | /** 413 | * Assert that the session has a given value. 414 | * 415 | * @return $this 416 | */ 417 | function (string|array $key, mixed $value = null): Expectation { 418 | $response = getTestableResponse($this); 419 | 420 | $response->assertSessionHas($key, $value); 421 | 422 | return $this; 423 | } 424 | ); 425 | 426 | expect()->extend( 427 | 'toHaveAllSession', 428 | /** 429 | * Assert that the session has a given list of values. 430 | */ 431 | function (array $bindings): Expectation { 432 | $response = getTestableResponse($this); 433 | 434 | $response->assertSessionHasAll($bindings); 435 | 436 | return $this; 437 | } 438 | ); 439 | 440 | expect()->extend( 441 | 'toHaveLocation', 442 | /** 443 | * Assert that the current location header matches the given URI. 444 | */ 445 | function (string $uri): Expectation { 446 | $response = getTestableResponse($this); 447 | 448 | $response->assertLocation($uri); 449 | 450 | return $this; 451 | }, 452 | ); 453 | -------------------------------------------------------------------------------- /src/Expectations/Storage.php: -------------------------------------------------------------------------------- 1 | extend( 12 | 'toExistInStorage', 13 | /** 14 | * Assert that the given file exist in storage. 15 | */ 16 | function (?string $disk = null): Expectation { 17 | $storageName = $disk ?? 'default'; 18 | 19 | assertTrue( 20 | Storage::disk($disk)->exists($this->value), 21 | "Failed asserting that $this->value exist in '$storageName' storage" 22 | ); 23 | 24 | return $this; 25 | } 26 | ); 27 | 28 | expect()->extend( 29 | 'toBeMissingInStorage', 30 | /** 31 | * Assert that the given file does not exist. 32 | * 33 | * @param mixed $value 34 | */ 35 | function (?string $disk = null): Expectation { 36 | $storageName = $disk ?? 'default'; 37 | 38 | assertFalse( 39 | Storage::disk($disk)->exists($this->value), 40 | "Failed asserting that $this->value is missing in '$storageName' storage" 41 | ); 42 | 43 | return $this; 44 | } 45 | ); 46 | -------------------------------------------------------------------------------- /src/Expectations/Time.php: -------------------------------------------------------------------------------- 1 | pipe( 14 | 'toBe', 15 | function (Closure $next, mixed $date) { 16 | try { 17 | $value = Carbon::make($this->value); 18 | $expected = Carbon::make($date); 19 | 20 | if (! $value instanceof Carbon) { 21 | return $next(); 22 | } 23 | 24 | if (is_string($this->value) && str($this->value)->startsWith('@') && $value->format('Y-m-d H:i:s') === '1970-01-01 00:00:00') { 25 | return $next(); 26 | } 27 | 28 | if (! $expected instanceof Carbon) { 29 | return $next(); 30 | } 31 | 32 | if (is_string($date) && str($date)->startsWith('@') && $value->format('Y-m-d H:i:s') === '1970-01-01 00:00:00') { 33 | return $next(); 34 | } 35 | } catch (Exception) { // @phpstan-ignore-line 36 | return $next(); 37 | } 38 | 39 | return expect($value->timestamp)->toBe($expected->timestamp, sprintf('Failed to assert that date [%s] is equal to [%s]', $value->toString(), $expected->toString())); 40 | } 41 | ); 42 | 43 | expect()->extend( 44 | 'toBeAfter', 45 | /** 46 | * Assert the date is after the given one. 47 | */ 48 | function (DateTimeInterface|string $date): Expectation { 49 | $value = ValueProcessor::getCarbonDate($this->value); 50 | $expected = ValueProcessor::getCarbonDate($date); 51 | 52 | assertTrue($value->isAfter($expected), sprintf('Failed to assert that [%s] is after %s', $value, $expected)); 53 | 54 | return $this; 55 | } 56 | ); 57 | 58 | expect()->extend( 59 | 'toBeBefore', 60 | /** 61 | * Assert the date is before the given one. 62 | */ 63 | function (DateTimeInterface|string $date): Expectation { 64 | $value = ValueProcessor::getCarbonDate($this->value); 65 | $expected = ValueProcessor::getCarbonDate($date); 66 | 67 | assertTrue($value->isBefore($expected), sprintf('Failed to assert that [%s] is before %s', $value, $expected)); 68 | 69 | return $this; 70 | } 71 | ); 72 | 73 | expect()->extend( 74 | 'toBeBirthday', 75 | /** 76 | * Assert the date a birthday. 77 | */ 78 | function (DateTimeInterface|string|null $date = null): Expectation { 79 | $value = ValueProcessor::getCarbonDate($this->value); 80 | 81 | if ($date !== null) { 82 | $date = ValueProcessor::getCarbonDate($date); 83 | } 84 | 85 | assertTrue($value->isBirthday($date), sprintf('Failed to assert that [%s] is a birthday', $value)); 86 | 87 | return $this; 88 | } 89 | ); 90 | 91 | expect()->extend( 92 | 'toBeCurrentDay', 93 | /** 94 | * Assert the date is today. 95 | */ 96 | function (): Expectation { 97 | $value = ValueProcessor::getCarbonDate($this->value); 98 | 99 | assertTrue($value->isCurrentDay(), sprintf('Failed to assert that [%s] is today', $value)); 100 | 101 | return $this; 102 | } 103 | ); 104 | 105 | expect()->extend( 106 | 'toBeCurrentHour', 107 | /** 108 | * Assert the date is in the current hour. 109 | */ 110 | function (): Expectation { 111 | $value = ValueProcessor::getCarbonDate($this->value); 112 | 113 | assertTrue($value->isCurrentHour(), sprintf('Failed to assert that [%s] is in the current hour', $value)); 114 | 115 | return $this; 116 | } 117 | ); 118 | 119 | expect()->extend( 120 | 'toBeCurrentMinute', 121 | /** 122 | * Assert the date is in the current minute. 123 | */ 124 | function (): Expectation { 125 | $value = ValueProcessor::getCarbonDate($this->value); 126 | 127 | assertTrue($value->isCurrentMinute(), sprintf('Failed to assert that [%s] is in the current minute', $value)); 128 | 129 | return $this; 130 | } 131 | ); 132 | 133 | expect()->extend( 134 | 'toBeCurrentMonth', 135 | /** 136 | * Assert the date is in the current month. 137 | */ 138 | function (): Expectation { 139 | $value = ValueProcessor::getCarbonDate($this->value); 140 | 141 | assertTrue($value->isCurrentMonth(), sprintf('Failed to assert that [%s] is in the current month', $value)); 142 | 143 | return $this; 144 | } 145 | ); 146 | 147 | expect()->extend( 148 | 'toBeCurrentSecond', 149 | /** 150 | * Assert the date is in the current second. 151 | */ 152 | function (): Expectation { 153 | $value = ValueProcessor::getCarbonDate($this->value); 154 | 155 | assertTrue($value->isCurrentSecond(), sprintf('Failed to assert that [%s] is in the current second', $value)); 156 | 157 | return $this; 158 | } 159 | ); 160 | 161 | expect()->extend( 162 | 'toBeCurrentWeek', 163 | /** 164 | * Assert the date is in the current week. 165 | */ 166 | function (): Expectation { 167 | $value = ValueProcessor::getCarbonDate($this->value); 168 | 169 | assertTrue($value->isCurrentWeek(), sprintf('Failed to assert that [%s] is in the current week', $value)); 170 | 171 | return $this; 172 | } 173 | ); 174 | 175 | expect()->extend( 176 | 'toBeCurrentYear', 177 | /** 178 | * Assert the date is in the current year. 179 | */ 180 | function (): Expectation { 181 | $value = ValueProcessor::getCarbonDate($this->value); 182 | 183 | assertTrue($value->isCurrentYear(), sprintf('Failed to assert that [%s] is in the current year', $value)); 184 | 185 | return $this; 186 | } 187 | ); 188 | 189 | expect()->extend( 190 | 'toBeEndOfDay', 191 | /** 192 | * Assert the date is end of day. 193 | */ 194 | function (): Expectation { 195 | $value = ValueProcessor::getCarbonDate($this->value); 196 | 197 | assertTrue($value->isEndOfDay(), sprintf('Failed to assert that [%s] is end of day', $value)); 198 | 199 | return $this; 200 | } 201 | ); 202 | 203 | expect()->extend( 204 | 'toBeFuture', 205 | /** 206 | * Assert the date is in the future. 207 | */ 208 | function (): Expectation { 209 | $value = ValueProcessor::getCarbonDate($this->value); 210 | 211 | assertTrue($value->isFuture(), sprintf('Failed to assert that [%s] is in the future', $value)); 212 | 213 | return $this; 214 | } 215 | ); 216 | 217 | expect()->extend( 218 | 'toBeLastMonth', 219 | /** 220 | * Assert the date is in the last month. 221 | */ 222 | function (): Expectation { 223 | $value = ValueProcessor::getCarbonDate($this->value); 224 | 225 | assertTrue($value->isLastMonth(), sprintf('Failed to assert that [%s] is in the last month', $value)); 226 | 227 | return $this; 228 | } 229 | ); 230 | 231 | expect()->extend( 232 | 'toBeLastWeek', 233 | /** 234 | * Assert the date is in the last week. 235 | */ 236 | function (): Expectation { 237 | $value = ValueProcessor::getCarbonDate($this->value); 238 | 239 | assertTrue($value->isLastWeek(), sprintf('Failed to assert that [%s] is in the last week', $value)); 240 | 241 | return $this; 242 | } 243 | ); 244 | 245 | expect()->extend( 246 | 'toBeLastYear', 247 | /** 248 | * Assert the date is in the last year. 249 | */ 250 | function (): Expectation { 251 | $value = ValueProcessor::getCarbonDate($this->value); 252 | 253 | assertTrue($value->isLastYear(), sprintf('Failed to assert that [%s] is in the last year', $value)); 254 | 255 | return $this; 256 | } 257 | ); 258 | 259 | expect()->extend( 260 | 'toBeMidday', 261 | /** 262 | * Assert the date midday. 263 | */ 264 | function (): Expectation { 265 | $value = ValueProcessor::getCarbonDate($this->value); 266 | 267 | assertTrue($value->isMidday(), sprintf('Failed to assert that [%s] is midday', $value)); 268 | 269 | return $this; 270 | } 271 | ); 272 | 273 | expect()->extend( 274 | 'toBeMidnight', 275 | /** 276 | * Assert the date is start of day / midnight. 277 | */ 278 | function (): Expectation { 279 | $value = ValueProcessor::getCarbonDate($this->value); 280 | 281 | assertTrue($value->isMidnight(), sprintf('Failed to assert that [%s] is midnight', $value)); 282 | 283 | return $this; 284 | } 285 | ); 286 | 287 | expect()->extend( 288 | 'toBeNextMonth', 289 | /** 290 | * Assert the date is in the next month. 291 | */ 292 | function (): Expectation { 293 | $value = ValueProcessor::getCarbonDate($this->value); 294 | 295 | assertTrue($value->isNextMonth(), sprintf('Failed to assert that [%s] is in the next month', $value)); 296 | 297 | return $this; 298 | } 299 | ); 300 | 301 | expect()->extend( 302 | 'toBeNextWeek', 303 | /** 304 | * Assert the date is in the next week. 305 | */ 306 | function (): Expectation { 307 | $value = ValueProcessor::getCarbonDate($this->value); 308 | 309 | assertTrue($value->isNextWeek(), sprintf('Failed to assert that [%s] is in the next week', $value)); 310 | 311 | return $this; 312 | } 313 | ); 314 | 315 | expect()->extend( 316 | 'toBeNextYear', 317 | /** 318 | * Assert the date is in the next year. 319 | */ 320 | function (): Expectation { 321 | $value = ValueProcessor::getCarbonDate($this->value); 322 | 323 | assertTrue($value->isNextYear(), sprintf('Failed to assert that [%s] is in the next year', $value)); 324 | 325 | return $this; 326 | } 327 | ); 328 | 329 | expect()->extend( 330 | 'toBePast', 331 | /** 332 | * Assert the date is in the past. 333 | */ 334 | function (): Expectation { 335 | $value = ValueProcessor::getCarbonDate($this->value); 336 | 337 | assertTrue($value->isPast(), sprintf('Failed to assert that [%s] is in the past', $value)); 338 | 339 | return $this; 340 | } 341 | ); 342 | 343 | expect()->extend( 344 | 'toBeSameDayAs', 345 | /** 346 | * Assert the date is the same day as the given one. 347 | */ 348 | function (DateTimeInterface|string $date): Expectation { 349 | $value = ValueProcessor::getCarbonDate($this->value); 350 | $expected = ValueProcessor::getCarbonDate($date); 351 | 352 | assertTrue($value->isSameDay($expected), sprintf('Failed to assert that [%s] is same day as %s', $value, $expected)); 353 | 354 | return $this; 355 | } 356 | ); 357 | 358 | expect()->extend( 359 | 'toBeSameHourAs', 360 | /** 361 | * Assert the date is the same hour as the given one. 362 | */ 363 | function (DateTimeInterface|string $date): Expectation { 364 | $value = ValueProcessor::getCarbonDate($this->value); 365 | $expected = ValueProcessor::getCarbonDate($date); 366 | 367 | assertTrue($value->isSameHour($expected), sprintf('Failed to assert that [%s] is same hour as %s', $value, $expected)); 368 | 369 | return $this; 370 | } 371 | ); 372 | 373 | expect()->extend( 374 | 'toBeSameMinuteAs', 375 | /** 376 | * Assert the date is the same minute as the given one. 377 | */ 378 | function (DateTimeInterface|string $date): Expectation { 379 | $value = ValueProcessor::getCarbonDate($this->value); 380 | $expected = ValueProcessor::getCarbonDate($date); 381 | 382 | assertTrue($value->isSameMinute($expected), sprintf('Failed to assert that [%s] is same minute as %s', $value, $expected)); 383 | 384 | return $this; 385 | } 386 | ); 387 | 388 | expect()->extend( 389 | 'toBeSameMonthAs', 390 | /** 391 | * Assert the date is the same month as the given one. 392 | */ 393 | function (DateTimeInterface|string $date): Expectation { 394 | $value = ValueProcessor::getCarbonDate($this->value); 395 | $expected = ValueProcessor::getCarbonDate($date); 396 | 397 | assertTrue($value->isSameMonth($expected), sprintf('Failed to assert that [%s] is same month as %s', $value, $expected)); 398 | 399 | return $this; 400 | } 401 | ); 402 | 403 | expect()->extend( 404 | 'toBeSameSecondAs', 405 | /** 406 | * Assert the date is the same second as the given one. 407 | */ 408 | function (DateTimeInterface|string $date): Expectation { 409 | $value = ValueProcessor::getCarbonDate($this->value); 410 | $expected = ValueProcessor::getCarbonDate($date); 411 | 412 | assertTrue($value->isSameSecond($expected), sprintf('Failed to assert that [%s] is same second as %s', $value, $expected)); 413 | 414 | return $this; 415 | } 416 | ); 417 | 418 | expect()->extend( 419 | 'toBeSameYearAs', 420 | /** 421 | * Assert the date is the same year as the given one. 422 | */ 423 | function (DateTimeInterface|string $date): Expectation { 424 | $value = ValueProcessor::getCarbonDate($this->value); 425 | $expected = ValueProcessor::getCarbonDate($date); 426 | 427 | assertTrue($value->isSameYear($expected), sprintf('Failed to assert that [%s] is same year as %s', $value, $expected)); 428 | 429 | return $this; 430 | } 431 | ); 432 | 433 | expect()->extend( 434 | 'toBeSameWeekAs', 435 | /** 436 | * Assert the date is the same week as the given one. 437 | */ 438 | function (DateTimeInterface|string $date): Expectation { 439 | $value = ValueProcessor::getCarbonDate($this->value); 440 | $expected = ValueProcessor::getCarbonDate($date); 441 | 442 | assertTrue($value->isSameWeek($expected), sprintf('Failed to assert that [%s] is same week as %s', $value, $expected)); 443 | 444 | return $this; 445 | } 446 | ); 447 | 448 | expect()->extend( 449 | 'toBeToday', 450 | /** 451 | * Assert the date is today. 452 | */ 453 | fn (): Expectation => $this->toBeCurrentDay() 454 | ); 455 | 456 | expect()->extend( 457 | 'toBeTomorrow', 458 | /** 459 | * Assert the date is tomorrow. 460 | */ 461 | function (): Expectation { 462 | $value = ValueProcessor::getCarbonDate($this->value); 463 | 464 | assertTrue($value->isTomorrow(), sprintf('Failed to assert that [%s] is tomorrow', $value)); 465 | 466 | return $this; 467 | } 468 | ); 469 | 470 | expect()->extend( 471 | 'toBeWeekday', 472 | /** 473 | * Assert the date is a weekday (between monday and friday). 474 | */ 475 | function (): Expectation { 476 | $value = ValueProcessor::getCarbonDate($this->value); 477 | 478 | assertTrue($value->isWeekday(), sprintf('Failed to assert that [%s] is a weekday', $value)); 479 | 480 | return $this; 481 | } 482 | ); 483 | 484 | expect()->extend( 485 | 'toBeStartOfDay', 486 | /** 487 | * Assert the date is start of day / midnight. 488 | */ 489 | function (): Expectation { 490 | $value = ValueProcessor::getCarbonDate($this->value); 491 | 492 | assertTrue($value->isStartOfDay(), sprintf('Failed to assert that [%s] is start of day', $value)); 493 | 494 | return $this; 495 | } 496 | ); 497 | 498 | expect()->extend( 499 | 'toBeWeekend', 500 | /** 501 | * Assert the date is Saturday or Sunday. 502 | */ 503 | function (): Expectation { 504 | $value = ValueProcessor::getCarbonDate($this->value); 505 | 506 | assertTrue($value->isWeekend(), sprintf('Failed to assert that [%s] is Saturday or Sunday', $value)); 507 | 508 | return $this; 509 | } 510 | ); 511 | 512 | expect()->extend( 513 | 'toBeTuesday', 514 | /** 515 | * Assert the date is Tuesday. 516 | */ 517 | function (): Expectation { 518 | $value = ValueProcessor::getCarbonDate($this->value); 519 | 520 | assertTrue($value->isTuesday(), sprintf('Failed to assert that [%s] is Tuesday', $value)); 521 | 522 | return $this; 523 | } 524 | ); 525 | 526 | expect()->extend( 527 | 'toBeMonday', 528 | /** 529 | * Assert the date is Monday. 530 | */ 531 | function (): Expectation { 532 | $value = ValueProcessor::getCarbonDate($this->value); 533 | 534 | assertTrue($value->isMonday(), sprintf('Failed to assert that [%s] is Monday', $value)); 535 | 536 | return $this; 537 | } 538 | ); 539 | 540 | expect()->extend( 541 | 'toBeWednesday', 542 | /** 543 | * Assert the date is Wednesday. 544 | */ 545 | function (): Expectation { 546 | $value = ValueProcessor::getCarbonDate($this->value); 547 | 548 | assertTrue($value->isWednesday(), sprintf('Failed to assert that [%s] is Wednesday', $value)); 549 | 550 | return $this; 551 | } 552 | ); 553 | 554 | expect()->extend( 555 | 'toBeThursday', 556 | /** 557 | * Assert the date is Thursday. 558 | */ 559 | function (): Expectation { 560 | $value = ValueProcessor::getCarbonDate($this->value); 561 | 562 | assertTrue($value->isThursday(), sprintf('Failed to assert that [%s] is Thursday', $value)); 563 | 564 | return $this; 565 | } 566 | ); 567 | 568 | expect()->extend( 569 | 'toBeFriday', 570 | /** 571 | * Assert the date is Friday. 572 | */ 573 | function (): Expectation { 574 | $value = ValueProcessor::getCarbonDate($this->value); 575 | 576 | assertTrue($value->isFriday(), sprintf('Failed to assert that [%s] is Friday', $value)); 577 | 578 | return $this; 579 | } 580 | ); 581 | 582 | expect()->extend( 583 | 'toBeSaturday', 584 | /** 585 | * Assert the date is Saturday. 586 | */ 587 | function (): Expectation { 588 | $value = ValueProcessor::getCarbonDate($this->value); 589 | 590 | assertTrue($value->isSaturday(), sprintf('Failed to assert that [%s] is Saturday', $value)); 591 | 592 | return $this; 593 | } 594 | ); 595 | 596 | expect()->extend( 597 | 'toBeSunday', 598 | /** 599 | * Assert the date is Sunday. 600 | */ 601 | function (): Expectation { 602 | $value = ValueProcessor::getCarbonDate($this->value); 603 | 604 | assertTrue($value->isSunday(), sprintf('Failed to assert that [%s] is Sunday', $value)); 605 | 606 | return $this; 607 | } 608 | ); 609 | 610 | expect()->extend( 611 | 'toBeYesterday', 612 | /** 613 | * Assert the date is yesterday. 614 | */ 615 | function (): Expectation { 616 | $value = ValueProcessor::getCarbonDate($this->value); 617 | 618 | assertTrue($value->isYesterday(), sprintf('Failed to assert that [%s] is yesterday', $value)); 619 | 620 | return $this; 621 | } 622 | ); 623 | -------------------------------------------------------------------------------- /src/Expectations/Views.php: -------------------------------------------------------------------------------- 1 | extend( 12 | 'toBeView', 13 | /** 14 | * Assert that the value is an instance of \Illuminate\View\View, and (optionally) that its name and data are the expected ones. 15 | */ 16 | function (string $name, array $data = []): Expectation { 17 | // @phpstan-ignore-next-line 18 | $this->toBeInstanceOf(\Illuminate\View\View::class) 19 | ->name()->toBe($name) 20 | ->getData()->toMatchArray($data); 21 | 22 | return $this; 23 | } 24 | ); 25 | -------------------------------------------------------------------------------- /src/Helpers/Models/RelationshipGuesser.php: -------------------------------------------------------------------------------- 1 | */ 28 | private array $triedRelationshipNames = []; 29 | 30 | private string $foundRelationshipName = ''; 31 | 32 | private bool $throwException = true; 33 | 34 | public static function from(Model $model): self 35 | { 36 | $guesser = new self; 37 | $guesser->model = $model; 38 | 39 | return $guesser; 40 | } 41 | 42 | public function to(Model $related): self 43 | { 44 | $this->related = $related; 45 | 46 | return $this; 47 | } 48 | 49 | public function ofType(string $relationshipClass): self 50 | { 51 | $this->relationshipClass = $relationshipClass; 52 | 53 | return $this; 54 | } 55 | 56 | public function withHint(string $relationshipName): self 57 | { 58 | $this->hintedRelationshipName = $relationshipName; 59 | 60 | return $this; 61 | } 62 | 63 | public function throwException(bool $throw): self 64 | { 65 | $this->throwException = $throw; 66 | 67 | return $this; 68 | } 69 | 70 | public function guess(): string 71 | { 72 | if ($this->hintedRelationshipName !== '' && $this->hintedRelationshipName !== '0') { 73 | $this->foundRelationshipName = $this->hintedRelationshipName; 74 | } 75 | 76 | if ($this->relationshipClass == BelongsTo::class) { 77 | $this->try(Str::camel(class_basename($this->related))); // @phpstan-ignore-line 78 | $this->try(Str::snake(class_basename($this->related))); // @phpstan-ignore-line 79 | } 80 | 81 | if ($this->relationshipClass == HasOne::class) { 82 | $this->try(Str::camel(class_basename($this->related))); // @phpstan-ignore-line 83 | $this->try(Str::snake(class_basename($this->related))); // @phpstan-ignore-line 84 | } 85 | 86 | if ($this->relationshipClass == HasMany::class) { 87 | $this->try(Str::camel(Str::plural(class_basename($this->related)))); // @phpstan-ignore-line 88 | $this->try(Str::snake(Str::plural(class_basename($this->related)))); // @phpstan-ignore-line 89 | } 90 | 91 | if ($this->throwException && ($this->foundRelationshipName === '' || $this->foundRelationshipName === '0')) { 92 | $triedNames = implode(' / ', array_unique($this->triedRelationshipNames)); 93 | throw new ExpectationFailedException(sprintf('Failed to assert that [%s] has relationship [%s]', $this->model::class, $triedNames)); // @phpstan-ignore-line 94 | } 95 | 96 | return $this->foundRelationshipName; 97 | } 98 | 99 | private function try(string $relationshipName): void 100 | { 101 | if ($relationshipName === '' || $relationshipName === '0') { 102 | return; 103 | } 104 | 105 | if ($this->foundRelationshipName !== '' && $this->foundRelationshipName !== '0') { 106 | return; 107 | } 108 | 109 | $this->triedRelationshipNames[] = $relationshipName; 110 | 111 | /* @phpstan-ignore-next-line */ 112 | if (method_exists($this->model, $relationshipName)) { 113 | $this->validateRelationship($relationshipName); 114 | $this->foundRelationshipName = $relationshipName; 115 | } 116 | } 117 | 118 | private function validateRelationship(string $relationshipName): void 119 | { 120 | if (! $this->model->{$relationshipName}() instanceof $this->relationshipClass) { 121 | throw new ExpectationFailedException(sprintf('Failed to assert that [%s] has relationship [%s] of type [%s]', $this->model::class, $relationshipName, $this->relationshipClass)); // @phpstan-ignore-line 122 | } 123 | } 124 | 125 | public function getRelationship(): HasOne|HasMany|BelongsTo 126 | { 127 | return $this->model->{$this->foundRelationshipName}(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Helpers/ValueProcessor.php: -------------------------------------------------------------------------------- 1 |