diff --git a/src/auth/src/Authenticatable.php b/src/auth/src/Authenticatable.php index 9032b5d689..46509d6859 100644 --- a/src/auth/src/Authenticatable.php +++ b/src/auth/src/Authenticatable.php @@ -71,7 +71,7 @@ public function getRememberToken(): ?string /** * Set the token value for the "remember me" session. */ - public function setRememberToken(string $value): void + public function setRememberToken(?string $value): void { if (! empty($this->getRememberTokenName())) { $this->{$this->getRememberTokenName()} = $value; diff --git a/src/auth/src/GenericUser.php b/src/auth/src/GenericUser.php index 736437ca73..6ad10780df 100755 --- a/src/auth/src/GenericUser.php +++ b/src/auth/src/GenericUser.php @@ -45,7 +45,7 @@ public function getAuthPasswordName(): string */ public function getAuthPassword(): ?string { - return $this->attributes[$this->getAuthPasswordName()]; + return $this->attributes[$this->getAuthPasswordName()] ?? null; } /** @@ -59,7 +59,7 @@ public function getRememberToken(): ?string /** * Set the "remember me" token value. */ - public function setRememberToken(string $value): void + public function setRememberToken(?string $value): void { $this->attributes[$this->getRememberTokenName()] = $value; } diff --git a/src/auth/src/Notifications/ResetPassword.php b/src/auth/src/Notifications/ResetPassword.php index 35db80786d..8293574820 100644 --- a/src/auth/src/Notifications/ResetPassword.php +++ b/src/auth/src/Notifications/ResetPassword.php @@ -10,6 +10,7 @@ use Hypervel\Container\Container; use Hypervel\Context\CoroutineContext; use Hypervel\Contracts\Config\Repository as ConfigContract; +use Hypervel\Contracts\Mail\Mailable; use Hypervel\Notifications\Messages\MailMessage; use Hypervel\Notifications\Notification; use Hypervel\Support\Facades\Lang; @@ -29,7 +30,7 @@ class ResetPassword extends Notification /** * The callback that should be used to build the mail message. * - * @var null|(Closure(mixed, string): MailMessage) + * @var null|(Closure(mixed, string): (Mailable|MailMessage)) */ public static ?Closure $toMailCallback = null; @@ -59,7 +60,7 @@ public function via(mixed $notifiable): array /** * Build the mail representation of the notification. */ - public function toMail(mixed $notifiable): MailMessage + public function toMail(mixed $notifiable): MailMessage|Mailable { if (static::$toMailCallback) { return call_user_func(static::$toMailCallback, $notifiable, $this->token); @@ -132,7 +133,7 @@ public static function createUrlUsing(Closure $callback): void * Boot-only. The callback persists in a static property for the worker * lifetime and runs on every reset-password notification. * - * @param Closure(mixed, string): MailMessage $callback + * @param Closure(mixed, string): (Mailable|MailMessage) $callback */ public static function toMailUsing(Closure $callback): void { diff --git a/src/auth/src/Notifications/VerifyEmail.php b/src/auth/src/Notifications/VerifyEmail.php index 907d8ecf9e..5e72f35e14 100644 --- a/src/auth/src/Notifications/VerifyEmail.php +++ b/src/auth/src/Notifications/VerifyEmail.php @@ -5,6 +5,7 @@ namespace Hypervel\Auth\Notifications; use Closure; +use Hypervel\Contracts\Mail\Mailable; use Hypervel\Notifications\Messages\MailMessage; use Hypervel\Notifications\Notification; use Hypervel\Support\CarbonImmutable; @@ -24,7 +25,7 @@ class VerifyEmail extends Notification /** * The callback that should be used to build the mail message. * - * @var null|(Closure(mixed, string): MailMessage) + * @var null|(Closure(mixed, string): (Mailable|MailMessage)) */ public static ?Closure $toMailCallback = null; @@ -39,7 +40,7 @@ public function via(mixed $notifiable): array /** * Build the mail representation of the notification. */ - public function toMail(mixed $notifiable): MailMessage + public function toMail(mixed $notifiable): MailMessage|Mailable { $verificationUrl = $this->verificationUrl($notifiable); @@ -100,7 +101,7 @@ public static function createUrlUsing(Closure $callback): void * Boot-only. The callback persists in a static property for the worker * lifetime and runs on every email-verification notification. * - * @param Closure(mixed, string): MailMessage $callback + * @param Closure(mixed, string): (Mailable|MailMessage) $callback */ public static function toMailUsing(Closure $callback): void { diff --git a/src/auth/src/SessionGuard.php b/src/auth/src/SessionGuard.php index a0a1a7e373..575f5ba4ed 100644 --- a/src/auth/src/SessionGuard.php +++ b/src/auth/src/SessionGuard.php @@ -315,7 +315,7 @@ public function basic(string $field = 'email', array $extraConditions = []): ?Re return null; } - return $this->failedBasicResponse(); + $this->failedBasicResponse(); } /** @@ -328,7 +328,7 @@ public function onceBasic(string $field = 'email', array $extraConditions = []): $credentials = $this->basicCredentials($this->getRequest(), $field); if (! $this->once(array_merge($credentials, $extraConditions))) { - return $this->failedBasicResponse(); + $this->failedBasicResponse(); } return null; diff --git a/src/broadcasting/src/AnonymousEvent.php b/src/broadcasting/src/AnonymousEvent.php index f5e34c2bdd..0342399381 100644 --- a/src/broadcasting/src/AnonymousEvent.php +++ b/src/broadcasting/src/AnonymousEvent.php @@ -137,6 +137,8 @@ public function broadcastWith(): array /** * Get the channels the event should broadcast on. + * + * @return Channel|Channel[]|string[] */ public function broadcastOn(): Channel|array { diff --git a/src/collections/src/Collection.php b/src/collections/src/Collection.php index 4be2997bf7..702d4c200b 100644 --- a/src/collections/src/Collection.php +++ b/src/collections/src/Collection.php @@ -1440,7 +1440,7 @@ public function hasSole(callable|string|null $key = null, mixed $operator = null /** * Get the first item in the collection but throw an exception if no matching items exist. * - * @param (callable(TValue, TKey): bool)|string $key + * @param null|(callable(TValue, TKey): bool)|string $key * @return TValue * * @throws ItemNotFoundException diff --git a/src/collections/src/Enumerable.php b/src/collections/src/Enumerable.php index 01fc40a741..e344957ebb 100644 --- a/src/collections/src/Enumerable.php +++ b/src/collections/src/Enumerable.php @@ -713,7 +713,7 @@ public function nth(int $step, int $offset = 0): static; /** * Get the items with the specified keys. * - * @param array|Enumerable|string $keys + * @param null|array|Enumerable|string $keys */ public function only(mixed $keys): static; @@ -1034,7 +1034,8 @@ public function pipeThrough(array $pipes): mixed; /** * Get the values of a given key. * - * @param array|string $value + * @param null|array|Closure|int|string $value + * @param null|array|Closure|int|string $key * @return static */ public function pluck(Closure|string|int|array|null $value, Closure|string|int|array|null $key = null): Collection|static; diff --git a/src/collections/src/LazyCollection.php b/src/collections/src/LazyCollection.php index 24d5615b94..626533b409 100644 --- a/src/collections/src/LazyCollection.php +++ b/src/collections/src/LazyCollection.php @@ -740,6 +740,7 @@ public function last(?callable $callback = null, mixed $default = null): mixed * Get the values of a given key. * * @param null|array|Closure|int|string $value + * @param null|array|Closure|int|string $key * @return static */ public function pluck(Closure|string|int|array|null $value, Closure|string|int|array|null $key = null): static @@ -1909,6 +1910,10 @@ protected function makeIterator(IteratorAggregate|Iterator|array|callable $sourc /** * Explode the "value" and "key" arguments passed to "pluck". + * + * @param null|array|Closure|int|string $value + * @param null|array|Closure|int|string $key + * @return array{null|array|Closure|int, null|array|Closure|int} */ protected function explodePluckParameters(Closure|string|int|array|null $value, Closure|string|int|array|null $key): array { diff --git a/src/console/src/Commands/ScheduleListCommand.php b/src/console/src/Commands/ScheduleListCommand.php index 3dd7239971..2f3760bca2 100644 --- a/src/console/src/Commands/ScheduleListCommand.php +++ b/src/console/src/Commands/ScheduleListCommand.php @@ -118,6 +118,7 @@ protected function displayJson(Collection $events, DateTimeZone $timezone): void 'has_mutex' => $event->mutex->exists($event), 'repeat_seconds' => $event->isRepeatable() ? $event->repeatSeconds : null, 'environments' => $event->environments, + 'on_one_server' => $event->onOneServer, ]; })->values()->toJson(), OutputInterface::OUTPUT_RAW); } diff --git a/src/container/src/Container.php b/src/container/src/Container.php index d59d95873d..d3a9b06465 100755 --- a/src/container/src/Container.php +++ b/src/container/src/Container.php @@ -243,7 +243,7 @@ class Container implements ContainerContract /** * The callback used to determine the container's environment. * - * @var null|(callable(array|string): bool|string) + * @var null|(callable(array|string): (bool|string)) */ protected $environmentResolver; diff --git a/src/contracts/src/Auth/Authenticatable.php b/src/contracts/src/Auth/Authenticatable.php index 7bf3497da6..d0b743dad4 100644 --- a/src/contracts/src/Auth/Authenticatable.php +++ b/src/contracts/src/Auth/Authenticatable.php @@ -34,7 +34,7 @@ public function getRememberToken(): ?string; /** * Set the token value for the "remember me" session. */ - public function setRememberToken(string $value): void; + public function setRememberToken(?string $value): void; /** * Get the column name for the "remember me" token. diff --git a/src/contracts/src/Mail/Mailable.php b/src/contracts/src/Mail/Mailable.php index a7231fc3b9..8380877d7e 100644 --- a/src/contracts/src/Mail/Mailable.php +++ b/src/contracts/src/Mail/Mailable.php @@ -52,7 +52,7 @@ public function to(array|object|string $address, ?string $name = null): static; * * @return $this */ - public function locale(string $locale): static; + public function locale(?string $locale): static; /** * Set the name of the mailer that should be used to send the message. diff --git a/src/contracts/src/Support/DeferringDisplayableValue.php b/src/contracts/src/Support/DeferringDisplayableValue.php index 4dae74aa76..0038ebfa92 100644 --- a/src/contracts/src/Support/DeferringDisplayableValue.php +++ b/src/contracts/src/Support/DeferringDisplayableValue.php @@ -9,5 +9,5 @@ interface DeferringDisplayableValue /** * Resolve the displayable value that the class is deferring. */ - public function resolveDisplayableValue(): Htmlable|string; + public function resolveDisplayableValue(): mixed; } diff --git a/src/database/src/Eloquent/Builder.php b/src/database/src/Eloquent/Builder.php index 9cb04ce1e7..42925cdab6 100644 --- a/src/database/src/Eloquent/Builder.php +++ b/src/database/src/Eloquent/Builder.php @@ -1673,7 +1673,7 @@ public function with(array|string $relations, Closure|string|null $callback = nu if ($callback instanceof Closure) { $eagerLoad = $this->parseWithRelations([$relations => $callback]); } else { - $eagerLoad = $this->parseWithRelations(is_string($relations) ? func_get_args() : $relations); + $eagerLoad = $this->parseWithRelations(is_string($relations) ? array_filter(func_get_args()) : $relations); } $this->eagerLoad = array_merge($this->eagerLoad, $eagerLoad); diff --git a/src/database/src/Eloquent/Casts/AsBinary.php b/src/database/src/Eloquent/Casts/AsBinary.php index 98a44e9cd8..c827dad7d0 100644 --- a/src/database/src/Eloquent/Casts/AsBinary.php +++ b/src/database/src/Eloquent/Casts/AsBinary.php @@ -6,6 +6,7 @@ use Hypervel\Contracts\Database\Eloquent\Castable; use Hypervel\Contracts\Database\Eloquent\CastsAttributes; +use Hypervel\Database\Eloquent\Model; use Hypervel\Support\BinaryCodec; use InvalidArgumentException; @@ -43,7 +44,7 @@ public function __construct(protected array $arguments) /** * Transform the attribute from the underlying model values. */ - public function get(mixed $model, string $key, mixed $value, array $attributes): ?string + public function get(Model $model, string $key, mixed $value, array $attributes): ?string { $attribute = $attributes[$key] ?? null; @@ -61,7 +62,7 @@ public function get(mixed $model, string $key, mixed $value, array $attributes): /** * Transform the attribute to its underlying model values. */ - public function set(mixed $model, string $key, mixed $value, array $attributes): array + public function set(Model $model, string $key, mixed $value, array $attributes): array { return [$key => BinaryCodec::encode($value, $this->format)]; } diff --git a/src/database/src/Eloquent/Casts/AsHtmlString.php b/src/database/src/Eloquent/Casts/AsHtmlString.php index 458c6314ea..217a5d0e4d 100644 --- a/src/database/src/Eloquent/Casts/AsHtmlString.php +++ b/src/database/src/Eloquent/Casts/AsHtmlString.php @@ -6,6 +6,8 @@ use Hypervel\Contracts\Database\Eloquent\Castable; use Hypervel\Contracts\Database\Eloquent\CastsAttributes; +use Hypervel\Contracts\Database\Eloquent\SerializesCastableAttributes; +use Hypervel\Database\Eloquent\Model; use Hypervel\Support\HtmlString; class AsHtmlString implements Castable @@ -17,13 +19,27 @@ class AsHtmlString implements Castable */ public static function castUsing(array $arguments): CastsAttributes { - return new class implements CastsAttributes { - public function get(mixed $model, string $key, mixed $value, array $attributes): ?HtmlString + return new class implements CastsAttributes, SerializesCastableAttributes { + /** + * Cast the stored value to an HTML string. + */ + public function get(Model $model, string $key, mixed $value, array $attributes): ?HtmlString { return isset($value) ? new HtmlString($value) : null; } - public function set(mixed $model, string $key, mixed $value, array $attributes): ?string + /** + * Prepare the HTML string for storage. + */ + public function set(Model $model, string $key, mixed $value, array $attributes): ?string + { + return isset($value) ? (string) $value : null; + } + + /** + * Serialize the HTML string for an array or JSON response. + */ + public function serialize(Model $model, string $key, mixed $value, array $attributes): ?string { return isset($value) ? (string) $value : null; } diff --git a/src/database/src/Eloquent/Casts/AsStringable.php b/src/database/src/Eloquent/Casts/AsStringable.php index 45c8ad5ab9..1bac8bdce8 100644 --- a/src/database/src/Eloquent/Casts/AsStringable.php +++ b/src/database/src/Eloquent/Casts/AsStringable.php @@ -6,6 +6,7 @@ use Hypervel\Contracts\Database\Eloquent\Castable; use Hypervel\Contracts\Database\Eloquent\CastsAttributes; +use Hypervel\Database\Eloquent\Model; use Hypervel\Support\Stringable; use Stringable as BaseStringable; @@ -19,12 +20,18 @@ class AsStringable implements Castable public static function castUsing(array $arguments): CastsAttributes { return new class implements CastsAttributes { - public function get(mixed $model, string $key, mixed $value, array $attributes): ?Stringable + /** + * Cast the stored value to a stringable instance. + */ + public function get(Model $model, string $key, mixed $value, array $attributes): ?Stringable { return isset($value) ? new Stringable($value) : null; } - public function set(mixed $model, string $key, mixed $value, array $attributes): ?string + /** + * Prepare the stringable value for storage. + */ + public function set(Model $model, string $key, mixed $value, array $attributes): ?string { return isset($value) ? (string) $value : null; } diff --git a/src/database/src/Eloquent/Casts/AsUri.php b/src/database/src/Eloquent/Casts/AsUri.php index 5619192885..f97b15d5dd 100644 --- a/src/database/src/Eloquent/Casts/AsUri.php +++ b/src/database/src/Eloquent/Casts/AsUri.php @@ -6,6 +6,7 @@ use Hypervel\Contracts\Database\Eloquent\Castable; use Hypervel\Contracts\Database\Eloquent\CastsAttributes; +use Hypervel\Database\Eloquent\Model; use Hypervel\Support\Uri; class AsUri implements Castable @@ -18,12 +19,18 @@ class AsUri implements Castable public static function castUsing(array $arguments): CastsAttributes { return new class implements CastsAttributes { - public function get(mixed $model, string $key, mixed $value, array $attributes): ?Uri + /** + * Cast the stored value to a URI. + */ + public function get(Model $model, string $key, mixed $value, array $attributes): ?Uri { return isset($value) ? new Uri($value) : null; } - public function set(mixed $model, string $key, mixed $value, array $attributes): ?string + /** + * Prepare the URI for storage. + */ + public function set(Model $model, string $key, mixed $value, array $attributes): ?string { return isset($value) ? (string) $value : null; } diff --git a/src/database/src/Eloquent/Collection.php b/src/database/src/Eloquent/Collection.php index 783cf8bf44..c8c164a8e3 100644 --- a/src/database/src/Eloquent/Collection.php +++ b/src/database/src/Eloquent/Collection.php @@ -257,7 +257,7 @@ public function loadMissing(array|string $relations): static $relations = func_get_args(); } - if ($this->isNotEmpty()) { + if ($this->isNotEmpty() && $relations !== []) { $query = $this->first()->newQueryWithoutRelationships()->with($relations); foreach ($query->getEagerLoads() as $key => $value) { diff --git a/src/database/src/Eloquent/Concerns/QueriesRelationships.php b/src/database/src/Eloquent/Concerns/QueriesRelationships.php index 7ad960d817..ca7ab0a1e3 100644 --- a/src/database/src/Eloquent/Concerns/QueriesRelationships.php +++ b/src/database/src/Eloquent/Concerns/QueriesRelationships.php @@ -699,7 +699,7 @@ protected function getMorphTypeForQuery(string $model): string /** * Add a "belongs to" relationship where clause to the query. * - * @param EloquentCollection|Model $related + * @param EloquentCollection|Model $related * * @throws InvalidArgumentException * @throws RelationNotFoundException @@ -743,6 +743,8 @@ public function whereBelongsTo(mixed $related, ?string $relationshipName = null, /** * Add a "BelongsTo" relationship with an "or where" clause to the query. + * + * @param EloquentCollection|Model $related */ public function orWhereBelongsTo(mixed $related, ?string $relationshipName = null): static { @@ -752,7 +754,7 @@ public function orWhereBelongsTo(mixed $related, ?string $relationshipName = nul /** * Add a "belongs to many" relationship where clause to the query. * - * @param EloquentCollection|Model $related + * @param EloquentCollection|Model $related * * @throws InvalidArgumentException * @throws RelationNotFoundException @@ -793,6 +795,8 @@ public function whereAttachedTo(mixed $related, ?string $relationshipName = null /** * Add a "belongs to many" relationship with an "or where" clause to the query. * + * @param EloquentCollection|Model $related + * * @throws RuntimeException */ public function orWhereAttachedTo(mixed $related, ?string $relationshipName = null): static diff --git a/src/database/src/Eloquent/ModelInspector.php b/src/database/src/Eloquent/ModelInspector.php index 172e73ee68..515f4331cf 100644 --- a/src/database/src/Eloquent/ModelInspector.php +++ b/src/database/src/Eloquent/ModelInspector.php @@ -8,6 +8,7 @@ use Hypervel\Contracts\Container\BindingResolutionException; use Hypervel\Contracts\Foundation\Application; use Hypervel\Database\Eloquent\Relations\Relation; +use Hypervel\Http\Resources\Json\JsonResource; use Hypervel\Support\Collection as BaseCollection; use Hypervel\Support\Facades\Gate; use Hypervel\Support\Str; @@ -285,7 +286,7 @@ protected function getBuilder(Model $model): string /** * Get the class used for JSON response transforming. * - * @return null|class-string + * @return null|class-string */ protected function getResource(Model $model): ?string { diff --git a/src/database/src/Eloquent/Relations/BelongsToMany.php b/src/database/src/Eloquent/Relations/BelongsToMany.php index 7a274321a8..fd142dca43 100644 --- a/src/database/src/Eloquent/Relations/BelongsToMany.php +++ b/src/database/src/Eloquent/Relations/BelongsToMany.php @@ -944,7 +944,12 @@ public function firstOr(Closure|array $columns = ['*'], ?Closure $callback = nul return $callback(); } - public function getResults() + /** + * Get the results of the relationship. + * + * @return EloquentCollection + */ + public function getResults(): BaseCollection { return ! is_null($this->parent->{$this->parentKey}) ? $this->get() diff --git a/src/database/src/LostConnectionDetector.php b/src/database/src/LostConnectionDetector.php index c2f3448761..5796fb5949 100644 --- a/src/database/src/LostConnectionDetector.php +++ b/src/database/src/LostConnectionDetector.php @@ -66,7 +66,7 @@ public function causedByLostConnection(Throwable $e): bool 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again', 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known', 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for', - 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected', + 'SSL SYSCALL error: EOF detected', 'SSL error: unexpected eof', 'SQLSTATE[HY000] [2002] Connection timed out', 'SSL: Connection timed out', diff --git a/src/database/src/Schema/Builder.php b/src/database/src/Schema/Builder.php index 87f2e058d0..b343a520bf 100755 --- a/src/database/src/Schema/Builder.php +++ b/src/database/src/Schema/Builder.php @@ -39,7 +39,7 @@ class Builder /** * The Blueprint resolver callback. * - * @var Closure(Connection, string, null|Closure): Blueprint + * @var null|(Closure(Connection, string, null|Closure): Blueprint) */ protected ?Closure $resolver = null; diff --git a/src/docs/authentication.md b/src/docs/authentication.md index f08113a5e1..53ffff24df 100644 --- a/src/docs/authentication.md +++ b/src/docs/authentication.md @@ -1123,7 +1123,7 @@ interface Authenticatable public function getAuthPasswordName(): string; public function getAuthPassword(): ?string; public function getRememberToken(): ?string; - public function setRememberToken(string $value): void; + public function setRememberToken(?string $value): void; public function getRememberTokenName(): string; } ``` diff --git a/src/docs/scheduling.md b/src/docs/scheduling.md index fef52fd6eb..6195364d64 100644 --- a/src/docs/scheduling.md +++ b/src/docs/scheduling.md @@ -69,7 +69,7 @@ If you would like to view an overview of your scheduled tasks and the next time php artisan schedule:list ``` -The `schedule:list` command also supports `--timezone`, `--environment`, `--next`, and `--json` options for customizing the displayed timezone, filtering by environment, sorting by the next due date, or returning the schedule as JSON. The `--environment` option may be repeated to include tasks from multiple environments: +The `schedule:list` command also supports `--timezone`, `--environment`, `--next`, and `--json` options for customizing the displayed timezone, filtering by environment, sorting by the next due date, or returning the schedule as JSON. JSON output includes an `on_one_server` field indicating whether each task is configured to run on one server. The `--environment` option may be repeated to include tasks from multiple environments: ```shell php artisan schedule:list --environment=staging --environment=production diff --git a/src/foundation/src/Exceptions/Handler.php b/src/foundation/src/Exceptions/Handler.php index 87723491f6..e127ab7fff 100644 --- a/src/foundation/src/Exceptions/Handler.php +++ b/src/foundation/src/Exceptions/Handler.php @@ -1023,8 +1023,10 @@ protected function invalid(Request $request, ValidationException $exception): Re $redirect = redirect($exception->redirectTo ?? url()->previous()); if (CoroutineContext::get(Store::CONTEXT_KEY)) { + $errorBag = $request->input('_error_bag'); + $redirect->withInput(Arr::except($request->input(), $this->dontFlash)) - ->withErrors($exception->errors(), $request->input('_error_bag', $exception->errorBag)); + ->withErrors($exception->errors(), is_string($errorBag) ? $errorBag : $exception->errorBag); } return $redirect; diff --git a/src/http/src/Client/Factory.php b/src/http/src/Client/Factory.php index 20169ae5a5..195a5d64a2 100644 --- a/src/http/src/Client/Factory.php +++ b/src/http/src/Client/Factory.php @@ -566,6 +566,8 @@ public function assertSequencesAreEmpty(): void /** * Get a collection of the request / response pairs matching the given truth test. + * + * @param null|(callable(Request, null|Response): bool) $callback */ public function recorded(?callable $callback = null): Collection { diff --git a/src/http/src/Client/PendingRequest.php b/src/http/src/Client/PendingRequest.php index 99144d2e61..c12e724a05 100644 --- a/src/http/src/Client/PendingRequest.php +++ b/src/http/src/Client/PendingRequest.php @@ -164,6 +164,8 @@ class PendingRequest implements Transient /** * The number of times to try the request. + * + * @var array|int */ protected array|int $tries = 1; diff --git a/src/http/src/Request.php b/src/http/src/Request.php index 0d17d194b5..abcdc46910 100644 --- a/src/http/src/Request.php +++ b/src/http/src/Request.php @@ -87,7 +87,7 @@ class Request extends SymfonyRequest implements Arrayable, ArrayAccess /** * All of the converted files for the request. * - * @var null|array + * @var null|array */ protected ?array $convertedFiles = null; diff --git a/src/log/src/Context/Repository.php b/src/log/src/Context/Repository.php index 298730b81d..bc27680543 100644 --- a/src/log/src/Context/Repository.php +++ b/src/log/src/Context/Repository.php @@ -237,7 +237,7 @@ public function push(string $key, mixed ...$values): static */ public function pop(string $key): mixed { - if (! $this->isStackable($key) || ! count($this->data[$key])) { + if (! $this->isStackable($key) || empty($this->data[$key])) { throw new RuntimeException("Unable to pop value from context stack for key [{$key}]."); } @@ -446,7 +446,7 @@ public function pushHidden(string $key, mixed ...$values): static */ public function popHidden(string $key): mixed { - if (! $this->isHiddenStackable($key) || ! count($this->hidden[$key])) { + if (! $this->isHiddenStackable($key) || empty($this->hidden[$key])) { throw new RuntimeException("Unable to pop value from hidden context stack for key [{$key}]."); } diff --git a/src/mail/src/Mailable.php b/src/mail/src/Mailable.php index dae258e77c..658ef7db27 100644 --- a/src/mail/src/Mailable.php +++ b/src/mail/src/Mailable.php @@ -50,7 +50,7 @@ class Mailable implements MailableContract, Renderable /** * The locale of the message. */ - public string $locale = ''; + public ?string $locale = null; /** * The person the message is from. @@ -85,22 +85,22 @@ class Mailable implements MailableContract, Renderable /** * The Markdown template for the message (if applicable). */ - public string $markdown; + public ?string $markdown = null; /** * The HTML to use for the message. */ - protected string $html; + protected ?string $html = null; /** * The view to use for the message. */ - public string $view; + public ?string $view = null; /** * The plain text view to use for the message. */ - public string $textView; + public ?string $textView = null; /** * The view data for the message. @@ -543,7 +543,7 @@ protected function runCallbacks(Message $message): static * * @return $this */ - public function locale(string $locale): static + public function locale(?string $locale): static { $this->locale = $locale; diff --git a/src/mail/src/PendingMail.php b/src/mail/src/PendingMail.php index b07389ebd9..3790a902b5 100644 --- a/src/mail/src/PendingMail.php +++ b/src/mail/src/PendingMail.php @@ -46,7 +46,7 @@ public function __construct( /** * Set the locale of the message. */ - public function locale(string $locale): static + public function locale(?string $locale): static { $this->locale = $locale; diff --git a/src/support/src/EncodedHtmlString.php b/src/support/src/EncodedHtmlString.php index d74d125309..d93d43bfb3 100644 --- a/src/support/src/EncodedHtmlString.php +++ b/src/support/src/EncodedHtmlString.php @@ -66,7 +66,7 @@ public function toHtml(): string $factory = CoroutineContext::get(self::ENCODER_CONTEXT_KEY) ?? static::$encodeUsingFactory; return ($factory ?? function ($value, $doubleEncode) { - return static::convert($value, doubleEncode: $doubleEncode); + return static::convert((string) ($value ?? ''), doubleEncode: $doubleEncode); })($value, $this->doubleEncode); } diff --git a/src/support/src/Facades/Http.php b/src/support/src/Facades/Http.php index f8d9cdada3..8c7ea7f9f7 100644 --- a/src/support/src/Facades/Http.php +++ b/src/support/src/Facades/Http.php @@ -43,7 +43,7 @@ * @method static bool preventingStrayRequests() * @method static \GuzzleHttp\Psr7\Response psr7Response(null|array|resource|\Psr\Http\Message\StreamInterface|string $body = null, int $status = 200, array $headers = []) * @method static \Hypervel\Http\Client\Factory record() - * @method static \Hypervel\Support\Collection recorded(callable|null $callback = null) + * @method static \Hypervel\Support\Collection recorded(null|callable $callback = null) * @method static void recordRequestResponsePair(\Hypervel\Http\Client\Request $request, \Hypervel\Http\Client\Response|null $response) * @method static \Hypervel\Http\Client\Factory registerConnection(string $name, array $config = []) * @method static \GuzzleHttp\Promise\PromiseInterface response(null|array|resource|\Psr\Http\Message\StreamInterface|string $body = null, int $status = 200, array $headers = []) diff --git a/src/support/src/Number.php b/src/support/src/Number.php index a1811d2620..2fbbcf28c6 100644 --- a/src/support/src/Number.php +++ b/src/support/src/Number.php @@ -311,6 +311,10 @@ public static function pairs(float|int $to, float|int $by, float|int $start = 0, $output[] = [$lower, $upper]; } + if ($output !== [] && $lower == $to && end($output)[1] < $to) { + $output[] = [$lower, $to]; + } + return $output; } diff --git a/src/support/src/Str.php b/src/support/src/Str.php index 1b0de18d8c..4d763d1ddd 100644 --- a/src/support/src/Str.php +++ b/src/support/src/Str.php @@ -427,6 +427,18 @@ public static function excerpt(string|int|float|bool|BaseStringable|null $text, */ public static function finish(string $value, string $cap): string { + if ($cap === '') { + return $value; + } + + if (! str_ends_with($value, $cap)) { + return $value . $cap; + } + + if (! str_ends_with($value, $cap . $cap)) { + return $value; + } + $quoted = preg_quote($cap, '/'); return preg_replace('/(?:' . $quoted . ')+$/u', '', $value) . $cap; @@ -1295,6 +1307,18 @@ public static function reverse(string $value): string */ public static function start(string $value, string $prefix): string { + if ($prefix === '') { + return $value; + } + + if (! str_starts_with($value, $prefix)) { + return $prefix . $value; + } + + if (! str_starts_with($value, $prefix . $prefix)) { + return $value; + } + $quoted = preg_quote($prefix, '/'); return $prefix . preg_replace('/^(?:' . $quoted . ')+/u', '', $value); diff --git a/src/validation/src/ConditionalRules.php b/src/validation/src/ConditionalRules.php index fe3cdb4724..ffa110a949 100644 --- a/src/validation/src/ConditionalRules.php +++ b/src/validation/src/ConditionalRules.php @@ -7,7 +7,9 @@ use Closure; use Hypervel\Contracts\Validation\Rule; use Hypervel\Contracts\Validation\ValidationRule; +use Hypervel\Support\Arr; use Hypervel\Support\Fluent; +use Stringable; class ConditionalRules { @@ -20,13 +22,13 @@ class ConditionalRules * Create a new conditional rules instance. * * @param bool|callable $condition the boolean condition indicating if the rules should be added to the attribute - * @param array|Closure|Rule|string|ValidationRule $rules the rules to be added to the attribute - * @param array|Closure|Rule|string|ValidationRule $defaultRules the rules to be added to the attribute if the condition fails + * @param array|Closure|Rule|string|Stringable|ValidationRule $rules the rules to be added to the attribute + * @param array|Closure|Rule|string|Stringable|ValidationRule $defaultRules the rules to be added to the attribute if the condition fails */ public function __construct( bool|callable $condition, - protected array|Closure|Rule|string|ValidationRule $rules, - protected array|Closure|Rule|string|ValidationRule $defaultRules = [] + protected array|Closure|Rule|string|Stringable|ValidationRule $rules, + protected array|Closure|Rule|string|Stringable|ValidationRule $defaultRules = [] ) { $this->condition = is_bool($condition) ? $condition : $condition(...); } @@ -43,25 +45,21 @@ public function passes(array $data = []): bool /** * Get the rules. - * - * @return array */ - public function rules(array $data = []): mixed + public function rules(array $data = []): array { - return is_string($this->rules) - ? explode('|', $this->rules) - : value($this->rules, new Fluent($data)); + $rules = $this->rules instanceof Closure ? ($this->rules)(new Fluent($data)) : $this->rules; + + return is_string($rules) ? explode('|', $rules) : Arr::wrap($rules); } /** * Get the default rules. - * - * @return array */ - public function defaultRules(array $data = []): mixed + public function defaultRules(array $data = []): array { - return is_string($this->defaultRules) - ? explode('|', $this->defaultRules) - : value($this->defaultRules, new Fluent($data)); + $rules = $this->defaultRules instanceof Closure ? ($this->defaultRules)(new Fluent($data)) : $this->defaultRules; + + return is_string($rules) ? explode('|', $rules) : Arr::wrap($rules); } } diff --git a/src/validation/src/Rule.php b/src/validation/src/Rule.php index 047ad9c7cd..ed242816cb 100644 --- a/src/validation/src/Rule.php +++ b/src/validation/src/Rule.php @@ -36,6 +36,7 @@ use Hypervel\Validation\Rules\StringRule; use Hypervel\Validation\Rules\Unique; use InvalidArgumentException; +use Stringable; use UnitEnum; class Rule @@ -63,8 +64,8 @@ public static function can(string $ability, mixed ...$arguments): Can */ public static function when( bool|callable $condition, - array|Closure|RuleContract|string|ValidationRule $rules, - array|Closure|RuleContract|string|ValidationRule $defaultRules = [] + array|Closure|RuleContract|string|Stringable|ValidationRule $rules, + array|Closure|RuleContract|string|Stringable|ValidationRule $defaultRules = [] ): ConditionalRules { return new ConditionalRules($condition, $rules, $defaultRules); } @@ -74,8 +75,8 @@ public static function when( */ public static function unless( bool|callable $condition, - array|Closure|RuleContract|string|ValidationRule $rules, - array|Closure|RuleContract|string|ValidationRule $defaultRules = [] + array|Closure|RuleContract|string|Stringable|ValidationRule $rules, + array|Closure|RuleContract|string|Stringable|ValidationRule $defaultRules = [] ): ConditionalRules { return new ConditionalRules($condition, $defaultRules, $rules); } diff --git a/src/view/src/Component.php b/src/view/src/Component.php index bd160b31d3..05f5c98c19 100644 --- a/src/view/src/Component.php +++ b/src/view/src/Component.php @@ -267,6 +267,8 @@ protected function createVariableFromMethod(ReflectionMethod $method): mixed /** * Create an invokable, toStringable variable for the given component method. + * + * @return InvokableComponentVariable */ protected function createInvokableVariable(string $method): InvokableComponentVariable { diff --git a/src/view/src/InvokableComponentVariable.php b/src/view/src/InvokableComponentVariable.php index 3e47a08ead..08e6f0c1fa 100644 --- a/src/view/src/InvokableComponentVariable.php +++ b/src/view/src/InvokableComponentVariable.php @@ -7,16 +7,22 @@ use ArrayIterator; use Closure; use Hypervel\Contracts\Support\DeferringDisplayableValue; -use Hypervel\Contracts\Support\Htmlable; use Hypervel\Support\Enumerable; use IteratorAggregate; use Stringable; use Traversable; +/** + * @template-covariant TValue = mixed + * + * @mixin TValue + */ class InvokableComponentVariable implements DeferringDisplayableValue, IteratorAggregate, Stringable { /** * Create a new variable instance. + * + * @param Closure(): TValue $callable */ public function __construct( protected Closure $callable @@ -25,14 +31,18 @@ public function __construct( /** * Resolve the displayable value that the class is deferring. + * + * @return TValue */ - public function resolveDisplayableValue(): Htmlable|string + public function resolveDisplayableValue(): mixed { return $this->__invoke(); } /** * Get an iterator instance for the variable. + * + * @return ArrayIterator */ public function getIterator(): Traversable { @@ -59,6 +69,8 @@ public function __call(string $method, array $parameters): mixed /** * Resolve the variable. + * + * @return TValue */ public function __invoke(): mixed { diff --git a/tests/Auth/AuthGuardTest.php b/tests/Auth/AuthGuardTest.php index d65bf5ff58..33725591d9 100755 --- a/tests/Auth/AuthGuardTest.php +++ b/tests/Auth/AuthGuardTest.php @@ -288,6 +288,29 @@ public function testLoginStoresPasswordHashInSession(): void $this->assertNotSame($sessionId, $session->getId()); } + public function testLoginWithGenericUserWithoutPasswordDoesNotStorePasswordHash(): void + { + [$guard, , $session] = $this->getRealGuard(); + $user = new GenericUser(['id' => 'foo']); + $warnings = []; + + set_error_handler(function (int $level, string $message) use (&$warnings): bool { + $warnings[] = $message; + + return true; + }, E_WARNING); + + try { + $guard->login($user); + } finally { + restore_error_handler(); + } + + $this->assertSame([], $warnings); + $this->assertSame('foo', $session->get($guard->getName())); + $this->assertFalse($session->has('password_hash_default')); + } + public function testSessionGuardIsMacroable(): void { $guard = $this->getGuard(); diff --git a/tests/Auth/AuthTokenGuardTest.php b/tests/Auth/AuthTokenGuardTest.php index 14bc5d3ada..08506fb788 100644 --- a/tests/Auth/AuthTokenGuardTest.php +++ b/tests/Auth/AuthTokenGuardTest.php @@ -586,7 +586,7 @@ public function getRememberToken(): ?string /** * Set the remember token. */ - public function setRememberToken(string $value): void + public function setRememberToken(?string $value): void { } diff --git a/tests/Auth/AuthenticatableTest.php b/tests/Auth/AuthenticatableTest.php index a6700bf375..db393be304 100644 --- a/tests/Auth/AuthenticatableTest.php +++ b/tests/Auth/AuthenticatableTest.php @@ -16,6 +16,15 @@ public function testItReturnsSameRememberTokenForString(): void $this->assertSame('sample_token', $user->getRememberToken()); } + public function testRememberTokenCanBeCleared(): void + { + $user = new User; + $user->setRememberToken('sample_token'); + $user->setRememberToken(null); + + $this->assertNull($user->getAttribute($user->getRememberTokenName())); + } + // REMOVED: testItReturnsStringAsRememberTokenWhenItWasSetToTrue // Tests implicit bool-to-string coercion which is a TypeError under strict_types. diff --git a/tests/Auth/GenericUserTest.php b/tests/Auth/GenericUserTest.php index 810d17c196..bcee3db3b5 100644 --- a/tests/Auth/GenericUserTest.php +++ b/tests/Auth/GenericUserTest.php @@ -53,6 +53,14 @@ public function testSetRememberTokenUpdatesToken(): void $this->assertSame('new', $user->getRememberToken()); } + public function testRememberTokenCanBeCleared(): void + { + $user = new GenericUser(['id' => 1, 'remember_token' => 'old']); + $user->setRememberToken(null); + + $this->assertNull($user->getRememberToken()); + } + public function testGetRememberTokenNameReturnsColumnName(): void { $user = new GenericUser(['id' => 1]); diff --git a/tests/Auth/ResetPasswordNotificationTest.php b/tests/Auth/ResetPasswordNotificationTest.php index 9d08681a3e..001ec69511 100644 --- a/tests/Auth/ResetPasswordNotificationTest.php +++ b/tests/Auth/ResetPasswordNotificationTest.php @@ -8,8 +8,10 @@ use Hypervel\Auth\Passwords\PasswordBroker; use Hypervel\Auth\Passwords\PasswordBrokerManager; use Hypervel\Context\CoroutineContext; +use Hypervel\Mail\Mailable; use Hypervel\Testbench\Attributes\WithConfig; use Hypervel\Testbench\TestCase; +use stdClass; #[WithConfig('auth.passwords.admins', [ 'driver' => 'database', @@ -19,6 +21,14 @@ ])] class ResetPasswordNotificationTest extends TestCase { + public function testMailCallbackCanReturnAMailable(): void + { + $mail = new Mailable; + ResetPassword::toMailUsing(fn (): Mailable => $mail); + + $this->assertSame($mail, (new ResetPassword('token'))->toMail(new stdClass)); + } + public function testExpiryIsCapturedFromSendingBrokerContext(): void { CoroutineContext::set(PasswordBroker::SENDING_BROKER_CONTEXT_KEY, 'admins'); diff --git a/tests/Auth/VerifyEmailNotificationTest.php b/tests/Auth/VerifyEmailNotificationTest.php index 21ffc688fe..8e0c370fec 100644 --- a/tests/Auth/VerifyEmailNotificationTest.php +++ b/tests/Auth/VerifyEmailNotificationTest.php @@ -5,6 +5,7 @@ namespace Hypervel\Tests\Auth; use Hypervel\Auth\Notifications\VerifyEmail; +use Hypervel\Mail\Mailable; use Hypervel\Routing\Router; use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; @@ -47,6 +48,14 @@ public function testMailMessageUsesTranslatedStringMetadata(): void $this->assertSame('Verify Email Address', $mail->actionText); } + public function testMailCallbackCanReturnAMailable(): void + { + $mail = new Mailable; + VerifyEmail::toMailUsing(fn (): Mailable => $mail); + + $this->assertSame($mail, (new VerifyEmail)->toMail(new VerifyEmailNotifiableStub)); + } + /** * Read the expiration timestamp from a signed URL. */ diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index 06064fde5e..3c399b4b06 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -838,7 +838,7 @@ public function getRememberToken(): ?string /** * Set the remember token for the user. */ - public function setRememberToken(string $value): void + public function setRememberToken(?string $value): void { } diff --git a/tests/Database/DatabaseConnectionLostTest.php b/tests/Database/DatabaseConnectionLostTest.php index 0637c497a1..5b6b6da6c9 100755 --- a/tests/Database/DatabaseConnectionLostTest.php +++ b/tests/Database/DatabaseConnectionLostTest.php @@ -23,9 +23,15 @@ public function testLostConnectionDetectorMatchesExceptionMessage(string $messag $this->assertTrue($detector->causedByLostConnection(new PDOException($message))); } + /** + * Provide lost database connection messages. + */ public static function shouldBeLostProvider(): array { return [ + 'SSL EOF' => ['SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected'], + 'PgBouncer crashed server' => ["SQLSTATE[HY000]: General error: 7 FATAL: server conn crashed?\nSSL SYSCALL error: EOF detected"], + 'PgBouncer idle timeout' => ["SQLSTATE[HY000]: General error: 7 FATAL: client_idle_timeout\nSSL SYSCALL error: EOF detected"], 'DNS failure' => ['SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again'], 'PostgreSQL connection refused' => ["SQLSTATE[08006] [7] connection to server at \"example.database.com\" (10.0.1.7), port 5432 failed: Connection refused\nIs the server running on that host and accepting TCP/IP connections? (Connection: pgsql, Host: example.database.com, Port: 5432, Database: forge, SQL: select * from \"cache\" where \"key\" in (hypervel:queue:restart))"], 'MySQL server gone away' => ['SQLSTATE[HY000]: General error: 2006 MySQL server has gone away'], @@ -33,6 +39,13 @@ public static function shouldBeLostProvider(): array ]; } + public function testLostConnectionDetectorDoesNotMatchUnrelatedEofMessage(): void + { + $detector = new LostConnectionDetector; + + $this->assertFalse($detector->causedByLostConnection(new PDOException('Unexpected EOF while parsing application data'))); + } + public function testQueryContextCannotForgeALostConnection(): void { $failure = new QueryException('test', 'insert into logs values (?)', ['Connection reset by peer'], new PDOException('Unique violation')); diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index cb92efaf11..4a4ee8becf 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -1046,6 +1046,13 @@ public function testEagerLoadParsingSetsProperRelationships(): void $this->assertInstanceOf(Closure::class, $eagers['orders']); $this->assertInstanceOf(Closure::class, $eagers['orders.lines']); + $builder = $this->getBuilder(); + $builder->with('orders', null); + $eagers = $builder->getEagerLoads(); + + $this->assertEquals(['orders'], array_keys($eagers)); + $this->assertInstanceOf(Closure::class, $eagers['orders']); + $builder = $this->getBuilder(); $builder->with(['orders.lines']); $eagers = $builder->getEagerLoads(); diff --git a/tests/Database/DatabaseEloquentCollectionTest.php b/tests/Database/DatabaseEloquentCollectionTest.php index 57d7c62f09..27b3df441e 100755 --- a/tests/Database/DatabaseEloquentCollectionTest.php +++ b/tests/Database/DatabaseEloquentCollectionTest.php @@ -349,6 +349,15 @@ public function testLoadMethodEagerLoadsGivenRelationships(): void $this->assertEquals(['results'], $c->all()); } + public function testLoadMissingWithoutRelationsDoesNotBuildAQuery(): void + { + $model = m::mock(Model::class); + $model->shouldNotReceive('newQueryWithoutRelationships'); + $collection = new Collection([$model]); + + $this->assertSame($collection, $collection->loadMissing([])); + } + public function testCollectionDictionaryReturnsModelKeys(): void { $one = (new CollectionModel)->forceFill(['id' => 1]); diff --git a/tests/Foundation/FoundationExceptionsHandlerTest.php b/tests/Foundation/FoundationExceptionsHandlerTest.php index b68621666c..e4d893aea8 100644 --- a/tests/Foundation/FoundationExceptionsHandlerTest.php +++ b/tests/Foundation/FoundationExceptionsHandlerTest.php @@ -819,6 +819,44 @@ function (array $argument) use (&$argumentActual): bool { $this->assertEquals($argumentExpected, $argumentActual); } + public function testInvalidUsesErrorBagWhenStringAndFallsBackWhenNotString(): void + { + $errorBagActual = null; + CoroutineContext::set(Store::CONTEXT_KEY, m::mock(Store::class)); + + $this->container->singleton('redirect', function () use (&$errorBagActual): Redirector { + $redirector = m::mock(Redirector::class); + $responder = m::mock(RedirectResponse::class . '[withInput,withErrors]', ['/']); + $redirector->expects('to')->twice()->andReturn($responder); + $responder->expects('withInput')->twice()->andReturn($responder); + $responder->expects('withErrors')->twice()->with( + m::any(), + m::on(function (string $errorBag) use (&$errorBagActual): bool { + $errorBagActual = $errorBag; + + return true; + }) + )->andReturn($responder); + + return $redirector; + }); + + $validator = m::mock(Validator::class); + $validator->expects('errors')->times(3)->andReturn(new MessageBag(['error' => 'My custom validation exception'])); + + $validationException = new ValidationException($validator); + $validationException->redirectTo = '/'; + $validationException->errorBag = 'default_bag'; + + $request = Request::create('/', 'POST', ['_error_bag' => 'custom_bag']); + $this->handler->render($request, $validationException); + $this->assertSame('custom_bag', $errorBagActual); + + $request = Request::create('/', 'POST', ['_error_bag' => ['invalid_array_bag']]); + $this->handler->render($request, $validationException); + $this->assertSame('default_bag', $errorBagActual); + } + public function testSuspiciousOperationReturns400WithoutReporting(): void { $this->config->set('app.debug', true); @@ -1551,7 +1589,7 @@ public function testValidateFailed(): void { $this->request->expects('expectsJson')->andReturn(false); $this->request->shouldReceive('input')->withNoArgs()->andReturn(['foo' => 'bar']); - $this->request->shouldReceive('input')->with('_error_bag', m::any())->andReturn('default'); + $this->request->shouldReceive('input')->with('_error_bag')->andReturn('default'); $session = m::mock(Store::class); $session->shouldReceive('get')->with('errors', m::type(ViewErrorBag::class))->andReturn(new MessageBag(['error' => 'My custom validation exception'])); diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index bc150a7320..f49481520c 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -111,6 +111,7 @@ public function testDisplayScheduleAsJson(): void $this->assertFalse($data[0]['has_mutex']); $this->assertIsArray($data[0]['environments']); $this->assertEmpty($data[0]['environments']); + $this->assertFalse($data[0]['on_one_server']); $this->assertSame('* * * * *', $data[2]['expression']); $this->assertSame('php artisan foobar a=' . ProcessUtils::escapeArgument('b'), $data[2]['command']); @@ -155,6 +156,24 @@ public function testDisplayScheduleAsJsonWithEnvironmentData(): void $this->assertContains($environment, $data[0]['environments']); } + public function testDisplayScheduleAsJsonWithOnOneServerData(): void + { + $this->schedule->command(FooCommand::class)->quarterly()->onOneServer(); + $this->schedule->command('inspire')->everyMinute(); + $this->schedule->call(fn () => '')->name('foo-callback')->everyMinute()->onOneServer(); + + $this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, ['--json' => true]); + $output = Artisan::output(); + + $this->assertJson($output); + $data = json_decode($output, true); + + $this->assertIsArray($data); + $this->assertCount(3, $data); + + $this->assertSame([true, false, true], array_column($data, 'on_one_server')); + } + public function testDisplayScheduleWithSortAsJson(): void { $this->schedule->command(FooCommand::class)->quarterly(); diff --git a/tests/Integration/Database/DatabaseCustomCastsTest.php b/tests/Integration/Database/DatabaseCustomCastsTest.php index c10b4b457c..aee6e9ef73 100644 --- a/tests/Integration/Database/DatabaseCustomCastsTest.php +++ b/tests/Integration/Database/DatabaseCustomCastsTest.php @@ -6,6 +6,7 @@ use Hypervel\Database\Eloquent\Casts\AsArrayObject; use Hypervel\Database\Eloquent\Casts\AsCollection; +use Hypervel\Database\Eloquent\Casts\AsHtmlString; use Hypervel\Database\Eloquent\Casts\AsStringable; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Schema\Blueprint; @@ -88,6 +89,22 @@ public function testCustomCasting(): void ); } + public function testAsHtmlStringIsSerializedAsString(): void + { + $model = new class extends Model { + protected array $casts = ['body' => AsHtmlString::class]; + }; + + $model->body = 'Taylor'; + + $this->assertSame(['body' => 'Taylor'], $model->toArray()); + $this->assertSame('{"body":"Taylor<\/b>"}', $model->toJson()); + + $model->body = null; + + $this->assertSame(['body' => null], $model->toArray()); + } + public function testCustomCastingUsingCreate(): void { $model = TestEloquentModelWithCustomCasts::create([ diff --git a/tests/Integration/Database/NestedTransactionRetryTest.php b/tests/Integration/Database/NestedTransactionRetryTest.php index 4042453406..ea6dcacaa3 100644 --- a/tests/Integration/Database/NestedTransactionRetryTest.php +++ b/tests/Integration/Database/NestedTransactionRetryTest.php @@ -103,12 +103,19 @@ public function testOnlyTheOuterTransactionRetriesAfterAManualSavepointRollback( DB::table('retry_records')->insert([['id' => 0, 'value' => 0], ['id' => 1, 'value' => 0]]); $ready = new Channel(2); $release = new Channel(2); + $finished = new Channel(2); $outerAttempts = $innerAttempts = [0, 0]; $committed = []; - $work = static function (int $worker) use ($ready, $release, &$outerAttempts, &$innerAttempts, &$committed): array { + $work = static function (int $worker) use ($ready, $release, $finished, &$outerAttempts, &$innerAttempts, &$committed): array { $connection = DB::connection(); - $connection->transaction(static function (Connection $connection) use ($worker, $ready, $release, &$outerAttempts, &$innerAttempts, &$committed): void { + $connection->transaction(static function (Connection $connection) use ($worker, $ready, $release, $finished, &$outerAttempts, &$innerAttempts, &$committed): void { $attempt = ++$outerAttempts[$worker]; + + // Let the surviving transaction commit before retrying, so the test causes exactly one deadlock. + if ($attempt > 1 && $finished->pop(5) !== true) { + throw new RuntimeException('The competing transaction did not finish before the retry.'); + } + $connection->table('retry_attempts')->insert(['worker' => $worker, 'attempt' => $attempt]); $connection->afterCommit(static function () use ($worker, $attempt, &$committed): void { $committed[$worker][] = $attempt; @@ -146,6 +153,8 @@ public function testOnlyTheOuterTransactionRetriesAfterAManualSavepointRollback( }, 3); }, 3); + $finished->push(true); + return [$connection->transactionLevel(), $connection->isReusable()]; }; diff --git a/tests/Integration/Log/ContextTest.php b/tests/Integration/Log/ContextTest.php index 55c172d8cd..9f3bf94053 100644 --- a/tests/Integration/Log/ContextTest.php +++ b/tests/Integration/Log/ContextTest.php @@ -240,6 +240,13 @@ public function testThrowsWhenPoppingFromEmptyList(): void Context::pop('breadcrumbs'); } + public function testThrowsWhenPoppingFromMissingKey(): void + { + $this->expectExceptionObject(new RuntimeException('Unable to pop value from context stack for key [breadcrumbs].')); + + Context::pop('breadcrumbs'); + } + public function testThrowsWhenPoppingFromNonListArray(): void { Context::add('breadcrumbs', ['foo' => 'bar']); @@ -267,6 +274,13 @@ public function testThrowsWhenPoppingFromEmptyHiddenList(): void Context::popHidden('breadcrumbs'); } + public function testThrowsWhenPoppingFromMissingHiddenKey(): void + { + $this->expectExceptionObject(new RuntimeException('Unable to pop value from hidden context stack for key [breadcrumbs].')); + + Context::popHidden('breadcrumbs'); + } + public function testThrowsWhenPoppingFromHiddenNonListArray(): void { Context::addHidden('breadcrumbs', ['foo' => 'bar']); diff --git a/tests/Integration/Mail/RenderingMailWithLocaleTest.php b/tests/Integration/Mail/RenderingMailWithLocaleTest.php index 9125fadef0..cf9b6f56fb 100644 --- a/tests/Integration/Mail/RenderingMailWithLocaleTest.php +++ b/tests/Integration/Mail/RenderingMailWithLocaleTest.php @@ -33,6 +33,20 @@ public function testMailableRendersInDefaultLocale(): void $this->assertStringContainsString('name', $mail->render()); } + public function testMailableViewsCanBeCleared(): void + { + $mail = (new Mailable)->markdown('missing')->view('missing')->text('view'); + $mail->markdown = null; + $mail->view = null; + + $this->assertStringContainsString('name', $mail->render()); + + $mail->textView = null; + $mail->view('view'); + + $this->assertStringContainsString('name', $mail->render()); + } + public function testMailableRendersInSelectedLocale(): void { $mail = (new RenderedTestMail)->locale('es'); diff --git a/tests/Integration/Mail/SendingMailWithLocaleTest.php b/tests/Integration/Mail/SendingMailWithLocaleTest.php index f4012c4c50..6b85844800 100644 --- a/tests/Integration/Mail/SendingMailWithLocaleTest.php +++ b/tests/Integration/Mail/SendingMailWithLocaleTest.php @@ -79,6 +79,28 @@ public function testMailIsSentWithLocaleFromMailable(): void ); } + public function testMailableLocaleCanBeCleared(): void + { + $mailable = (new TestMail)->locale('ar')->locale(null); + + Mail::to('test@mail.com')->send($mailable); + + $this->assertStringContainsString( + 'name', + $this->app->make('mailer')->getSymfonyTransport()->messages()[0]->toString() + ); + } + + public function testPendingMailLocaleCanBeCleared(): void + { + Mail::to('test@mail.com')->locale('ar')->locale(null)->send(new TestMail); + + $this->assertStringContainsString( + 'name', + $this->app->make('mailer')->getSymfonyTransport()->messages()[0]->toString() + ); + } + public function testMailIsSentWithLocaleUpdatedListenersCalled(): void { CarbonImmutable::setTestNow('2018-04-01'); diff --git a/tests/Integration/Mail/SendingMarkdownMailTest.php b/tests/Integration/Mail/SendingMarkdownMailTest.php index 952bbbcd2d..f1cb82e5f6 100644 --- a/tests/Integration/Mail/SendingMarkdownMailTest.php +++ b/tests/Integration/Mail/SendingMarkdownMailTest.php @@ -286,7 +286,7 @@ public function content(): Content class MarkdownBasicMailableWithTextView extends Mailable { - public string $textView = 'text'; + public ?string $textView = 'text'; public function envelope(): Envelope { diff --git a/tests/Jwt/ClaimFactoryTest.php b/tests/Jwt/ClaimFactoryTest.php index 8e94a9f11e..eadc113367 100644 --- a/tests/Jwt/ClaimFactoryTest.php +++ b/tests/Jwt/ClaimFactoryTest.php @@ -387,7 +387,10 @@ public function getRememberToken(): ?string return null; } - public function setRememberToken(string $value): void + /** + * Set the remember token. + */ + public function setRememberToken(?string $value): void { } diff --git a/tests/Jwt/JwtGuardCoroutineSafetyTest.php b/tests/Jwt/JwtGuardCoroutineSafetyTest.php index c7e36decb9..dcb0e5b5fc 100644 --- a/tests/Jwt/JwtGuardCoroutineSafetyTest.php +++ b/tests/Jwt/JwtGuardCoroutineSafetyTest.php @@ -198,7 +198,10 @@ public function getRememberToken(): ?string return null; } - public function setRememberToken(string $value): void + /** + * Set the remember token. + */ + public function setRememberToken(?string $value): void { } diff --git a/tests/Jwt/JwtGuardSubjectLockTest.php b/tests/Jwt/JwtGuardSubjectLockTest.php index 87f4210daa..50e0558cc4 100644 --- a/tests/Jwt/JwtGuardSubjectLockTest.php +++ b/tests/Jwt/JwtGuardSubjectLockTest.php @@ -156,7 +156,10 @@ public function getRememberToken(): ?string return null; } - public function setRememberToken(string $value): void + /** + * Set the remember token. + */ + public function setRememberToken(?string $value): void { } diff --git a/tests/Mail/MarkdownCoroutineSafetyTest.php b/tests/Mail/MarkdownCoroutineSafetyTest.php index b5fa48b4d3..2ca7fdbba4 100644 --- a/tests/Mail/MarkdownCoroutineSafetyTest.php +++ b/tests/Mail/MarkdownCoroutineSafetyTest.php @@ -472,7 +472,7 @@ public function renderText(string $view, array $data = []): HtmlString class ThemeProbeMailable extends Mailable { - public string $markdown = 'message'; + public ?string $markdown = 'message'; public function __construct(string $theme) { diff --git a/tests/Pagination/PaginatorTest.php b/tests/Pagination/PaginatorTest.php index 906e345705..692cadb712 100644 --- a/tests/Pagination/PaginatorTest.php +++ b/tests/Pagination/PaginatorTest.php @@ -277,24 +277,24 @@ public function testCurrentPageResolverReceivesTheDefaultPageName(): void $this->assertSame(2, $paginator->currentPage()); } - public function testCurrentPageResolverAndUrlsUseTheConfiguredPageName(): void + public function testCurrentPageIsResolvedUsingPageName(): void { $resolvedPageName = null; Paginator::currentPageResolver(function (string $pageName) use (&$resolvedPageName): int { $resolvedPageName = $pageName; - return 2; + return ['page' => 3, 'comments' => 2][$pageName]; }); $paginator = new Paginator( ['first', 'second'], 1, - options: ['pageName' => 'users'], + options: ['pageName' => 'comments'], ); - $this->assertSame('users', $resolvedPageName); + $this->assertSame('comments', $resolvedPageName); $this->assertSame(2, $paginator->currentPage()); - $this->assertSame('/?users=2', $paginator->url($paginator->currentPage())); + $this->assertSame('/?comments=2', $paginator->url($paginator->currentPage())); } public function testPaginatorJsonThrowsForInvalidUtf8(): void diff --git a/tests/Queue/QueueManagerTest.php b/tests/Queue/QueueManagerTest.php index 96eac97d23..41999d9c2e 100644 --- a/tests/Queue/QueueManagerTest.php +++ b/tests/Queue/QueueManagerTest.php @@ -21,6 +21,7 @@ use Hypervel\Queue\Connectors\SyncConnector; use Hypervel\Queue\Jobs\Job; use Hypervel\Queue\NullQueue; +use Hypervel\Queue\Queue as BaseQueue; use Hypervel\Queue\QueueManager; use Hypervel\Queue\QueuePoolProxy; use Hypervel\Queue\SyncQueue; @@ -28,6 +29,7 @@ use InvalidArgumentException; use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; +use ReflectionProperty; use RuntimeException; class QueueManagerTest extends TestCase @@ -152,6 +154,21 @@ public function testEnumConnectionCanBeChecked(): void $this->assertTrue($manager->connected(QueueConnectionName::Sync)); } + public function testCreatePayloadUsingDoesNotResolveTheDefaultConnection(): void + { + $container = $this->getContainer(); + $container->make('config')->set('queue.default', 'missing'); + + $manager = new QueueManager($container); + $manager->createPayloadUsing(static fn (?string $connection, ?string $queue, ?array $payload): array => ['foo' => 'bar']); + + $this->assertFalse($manager->connected('missing')); + + $callbacks = (new ReflectionProperty(BaseQueue::class, 'createPayloadCallbacks'))->getValue(); + + $this->assertSame(['foo' => 'bar'], $callbacks[array_key_last($callbacks)](null, null, null)); + } + public function testSetDefaultDriverAcceptsBackedEnum(): void { $container = $this->getContainer(); @@ -494,18 +511,6 @@ public function testSetApplicationUpdatesCachedDirectQueueInPlace(): void $this->assertSame($queue, $manager->connection('sync')); } - public function testPayloadCallbacksCanBeRegisteredWithoutResolvingTheDefaultConnection(): void - { - $container = $this->getContainer(); - $container->make('config')->set('queue.default', 'missing'); - - $manager = new QueueManager($container); - - $manager->createPayloadUsing(static fn (string $connection, ?string $queue, array $payload): array => []); - - $this->assertFalse($manager->connected('missing')); - } - /** * Create a manager with real failover and null connectors. */ diff --git a/tests/Sanctum/AuthenticateSessionTest.php b/tests/Sanctum/AuthenticateSessionTest.php index bca9370d30..c3fcf187e1 100644 --- a/tests/Sanctum/AuthenticateSessionTest.php +++ b/tests/Sanctum/AuthenticateSessionTest.php @@ -423,7 +423,10 @@ public function getRememberToken(): ?string return null; } - public function setRememberToken(string $value): void + /** + * Set the remember token. + */ + public function setRememberToken(?string $value): void { } diff --git a/tests/Sanctum/Fixtures/DummyAuthenticatable.php b/tests/Sanctum/Fixtures/DummyAuthenticatable.php index cb69ea7f8c..e35ecb4b11 100644 --- a/tests/Sanctum/Fixtures/DummyAuthenticatable.php +++ b/tests/Sanctum/Fixtures/DummyAuthenticatable.php @@ -37,7 +37,10 @@ public function getRememberToken(): ?string return null; } - public function setRememberToken(string $value): void + /** + * Set the remember token. + */ + public function setRememberToken(?string $value): void { } diff --git a/tests/Support/SupportEncodedHtmlStringTest.php b/tests/Support/SupportEncodedHtmlStringTest.php index ee9dfb2042..5eaa35f503 100644 --- a/tests/Support/SupportEncodedHtmlStringTest.php +++ b/tests/Support/SupportEncodedHtmlStringTest.php @@ -6,16 +6,17 @@ use Hypervel\Support\EncodedHtmlString; use Hypervel\Tests\TestCase; +use Hypervel\View\InvokableComponentVariable; use function Hypervel\Coroutine\parallel; class SupportEncodedHtmlStringTest extends TestCase { - protected function tearDown(): void + public function testNumericAndDeferredValuesCanBeEncoded(): void { - EncodedHtmlString::flushState(); - - parent::tearDown(); + $this->assertSame('42', (new EncodedHtmlString(42))->toHtml()); + $this->assertSame('1.5', (new EncodedHtmlString(1.5))->toHtml()); + $this->assertSame('42', (new EncodedHtmlString(new InvokableComponentVariable(fn () => 42)))->toHtml()); } public function testScopedEncodingOverridesBootEncoderAndRestoresNestedState(): void diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index 7485b04ba7..570dc647ac 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -412,12 +412,22 @@ public function testPairs(): void $this->assertSame([[1, 1001], [1001, 2001], [2001, 2500]], Number::pairs(2500, 1000, 1, 0)); $this->assertSame([[1, 1000], [1001, 2000], [2001, 2500]], Number::pairs(2500, 1000, 1, 1)); $this->assertSame([[0, 2.5], [2.5, 5.0], [5.0, 7.5], [7.5, 10.0]], Number::pairs(10, 2.5, 0, 0)); - $this->assertSame([[0, 2.0], [2.5, 4.5], [5.0, 7.0], [7.5, 9.5]], Number::pairs(10, 2.5, 0, 0.5)); + $this->assertSame([[0, 2.0], [2.5, 4.5], [5.0, 7.0], [7.5, 9.5], [10.0, 10]], Number::pairs(10, 2.5, 0, 0.5)); $this->assertSame([[0.5, 3.0], [3.0, 5.5], [5.5, 8.0], [8.0, 10]], Number::pairs(10, 2.5, 0.5, 0)); $this->assertSame([[0.5, 2.5], [3.0, 5.0], [5.5, 7.5], [8.0, 10.0]], Number::pairs(10, 2.5, 0.5, 0.5)); $this->assertSame([[0, 9], [10, 19], [20, 25]], Number::pairs(25, 10)); } + public function testPairsIncludesUpperBoundWhenItStartsANewPair(): void + { + $this->assertSame([[0, 9], [10, 19], [20, 20]], Number::pairs(20, 10)); + $this->assertSame([[0, 9], [10, 19], [20, 21]], Number::pairs(21, 10)); + $this->assertSame([[1, 3], [4, 6], [7, 9], [10, 10]], Number::pairs(10, 3, 1)); + $this->assertSame([[0, 10], [10, 20]], Number::pairs(20, 10, 0, 0)); + $this->assertSame([], Number::pairs(0, 10)); + $this->assertSame([], Number::pairs(5, 10, 10)); + } + public function testPairsThrowsWhenByIsZero(): void { $this->expectException(InvalidArgumentException::class); diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index a8d1578d03..0c631f59cf 100644 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -700,6 +700,8 @@ public function testStrStart(): void $this->assertSame('/test/string', Str::start('test/string', '/')); $this->assertSame('/test/string', Str::start('/test/string', '/')); $this->assertSame('/test/string', Str::start('//test/string', '/')); + $this->assertSame('test/string', Str::start('test/string', '')); + $this->assertSame('', Str::start('', '')); } public function testFlushCache(): void @@ -715,6 +717,11 @@ public function testFinish(): void $this->assertSame('abbc', Str::finish('ab', 'bc')); $this->assertSame('abbc', Str::finish('abbcbc', 'bc')); $this->assertSame('abcbbc', Str::finish('abcbbcbc', 'bc')); + $this->assertSame('test/string/', Str::finish('test/string', '/')); + $this->assertSame('test/string/', Str::finish('test/string/', '/')); + $this->assertSame('test/string/', Str::finish('test/string//', '/')); + $this->assertSame('test/string', Str::finish('test/string', '')); + $this->assertSame('', Str::finish('', '')); } public function testWrap(): void diff --git a/tests/Telescope/Http/AuthorizationTest.php b/tests/Telescope/Http/AuthorizationTest.php index e308b81ae0..44c525ad68 100644 --- a/tests/Telescope/Http/AuthorizationTest.php +++ b/tests/Telescope/Http/AuthorizationTest.php @@ -143,7 +143,10 @@ public function getRememberToken(): ?string return null; } - public function setRememberToken(string $value): void + /** + * Set the remember token. + */ + public function setRememberToken(?string $value): void { } diff --git a/tests/Telescope/Http/AvatarTest.php b/tests/Telescope/Http/AvatarTest.php index b7d7b6f515..0d7f795ec4 100644 --- a/tests/Telescope/Http/AvatarTest.php +++ b/tests/Telescope/Http/AvatarTest.php @@ -173,7 +173,10 @@ public function getRememberToken(): ?string return null; } - public function setRememberToken(string $value): void + /** + * Set the remember token. + */ + public function setRememberToken(?string $value): void { } diff --git a/tests/Telescope/Watchers/GateWatcherTest.php b/tests/Telescope/Watchers/GateWatcherTest.php index 540cf60dbe..c25e52877b 100644 --- a/tests/Telescope/Watchers/GateWatcherTest.php +++ b/tests/Telescope/Watchers/GateWatcherTest.php @@ -374,7 +374,10 @@ public function getRememberToken(): ?string return 'i-am-telescope'; } - public function setRememberToken(string $value): void + /** + * Set the remember token. + */ + public function setRememberToken(?string $value): void { } diff --git a/tests/Validation/ValidationRuleParserTest.php b/tests/Validation/ValidationRuleParserTest.php index 31bbca1746..abacb94776 100644 --- a/tests/Validation/ValidationRuleParserTest.php +++ b/tests/Validation/ValidationRuleParserTest.php @@ -116,6 +116,34 @@ public function testConditionalRulesAcceptNonClosureCallableConditions(): void ], $rules); } + #[TestWith(['when', true, 'in:"allowed"'])] + #[TestWith(['when', false, 'in:"fallback"'])] + #[TestWith(['unless', true, 'in:"fallback"'])] + #[TestWith(['unless', false, 'in:"allowed"'])] + public function testConditionalRulesAcceptStringableRules(string $method, bool $condition, string $expected): void + { + $rule = Rule::{$method}($condition, Rule::in(['allowed']), Rule::in(['fallback'])); + + $rules = ValidationRuleParser::filterConditionalRules([ + 'direct' => $rule, + 'nested' => [$rule], + ]); + + $this->assertSame($expected, (string) $rules['direct'][0]); + $this->assertSame($expected, (string) $rules['nested'][0]); + } + + #[TestWith([true])] + #[TestWith([false])] + public function testConditionalRuleCallbacksReturnRuleLists(bool $condition): void + { + $rule = Rule::when($condition, fn () => 'required|min:2', fn () => 'nullable|max:10'); + + $this->assertSame([ + 'name' => $condition ? ['required', 'min:2'] : ['nullable', 'max:10'], + ], ValidationRuleParser::filterConditionalRules(['name' => $rule])); + } + /** * Determine if the conditional test rules apply. */ diff --git a/tests/Validation/ValidationUniqueRuleTest.php b/tests/Validation/ValidationUniqueRuleTest.php index 7f5aa9c505..148de3bd57 100644 --- a/tests/Validation/ValidationUniqueRuleTest.php +++ b/tests/Validation/ValidationUniqueRuleTest.php @@ -194,6 +194,10 @@ public function testItHandlesWhereWithSpecialValues(): void $rule = new Unique('table', 'column'); $rule->where('foo', 0); $this->assertSame('unique:table,column,NULL,id,foo,"0"', (string) $rule); + + $rule = new Unique('table', 'column'); + $rule->where('foo', 0.13); + $this->assertSame('unique:table,column,NULL,id,foo,"0.13"', (string) $rule); } public function testItValidatesUniqueRuleWithWhereInAndWhereNotIn(): void diff --git a/tests/View/Blade/BladeEchoTest.php b/tests/View/Blade/BladeEchoTest.php index 113011d2f1..54c067c595 100644 --- a/tests/View/Blade/BladeEchoTest.php +++ b/tests/View/Blade/BladeEchoTest.php @@ -22,8 +22,6 @@ public function testEchosAreCompiled(): void }}')); $this->assertSame("\n\n", $this->compiler->compileString("{{ \$name }}\n")); $this->assertSame("\r\n\r\n", $this->compiler->compileString("{{ \$name }}\r\n")); - $this->assertSame("\n\n", $this->compiler->compileString("{{ \$name }}\n")); - $this->assertSame("\r\n\r\n", $this->compiler->compileString("{{ \$name }}\r\n")); $this->assertSame( '', diff --git a/tests/View/Blade/BladeElseStatementsTest.php b/tests/View/Blade/BladeElseStatementsTest.php index c437af0809..39f64a0ef7 100644 --- a/tests/View/Blade/BladeElseStatementsTest.php +++ b/tests/View/Blade/BladeElseStatementsTest.php @@ -17,21 +17,6 @@ public function testElseStatementsAreCompiled(): void breeze boom -'; - $this->assertEquals($expected, $this->compiler->compileString($string)); - } - - public function testElseIfStatementsAreCompiled(): void - { - $string = '@if(name(foo(bar))) -breeze -@elseif(boom(breeze)) -boom -@endif'; - $expected = ' -breeze - -boom '; $this->assertEquals($expected, $this->compiler->compileString($string)); } diff --git a/tests/View/Blade/BladePhpStatementsTest.php b/tests/View/Blade/BladePhpStatementsTest.php index b5d2038a10..a2b50a9b52 100644 --- a/tests/View/Blade/BladePhpStatementsTest.php +++ b/tests/View/Blade/BladePhpStatementsTest.php @@ -158,10 +158,6 @@ public function testNestedTagCalls(): void $expected = ''; $this->assertEquals($expected, $this->compiler->compileString($string)); - $string = " @empty(\$v), 't' => @empty(\$v1)])>"; - $expected = ''; - $this->assertEquals($expected, $this->compiler->compileString($string)); - $string = " @empty(\$v), 't' => @empty(\$v1), 'r' => @empty(\$v2)])>"; $expected = ''; $this->assertEquals($expected, $this->compiler->compileString($string)); diff --git a/tests/View/ComponentTest.php b/tests/View/ComponentTest.php index 28d2934dcd..a6fc18e919 100644 --- a/tests/View/ComponentTest.php +++ b/tests/View/ComponentTest.php @@ -604,14 +604,6 @@ public function render(): ViewContract|Htmlable|Closure|string } } -class TestInlineViewComponentWithoutDependencies extends Component -{ - public function render(): ViewContract|Htmlable|Closure|string - { - return 'alert'; - } -} - class TestStatefulInlineComponent extends Component { public static int $constructed = 0; diff --git a/tests/View/ViewBladeCompilerTest.php b/tests/View/ViewBladeCompilerTest.php index 91de42ad35..9cb5ae05f4 100644 --- a/tests/View/ViewBladeCompilerTest.php +++ b/tests/View/ViewBladeCompilerTest.php @@ -87,7 +87,7 @@ public function testIsExpiredReturnsFalseWhenUseCacheIsTrueAndNoFileModification public function testIsExpiredReturnsTrueWhenUseCacheIsFalse(): void { - $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__, shouldCache: false); + $compiler = new BladeCompiler($this->getFiles(), __DIR__, shouldCache: false); $this->assertTrue($compiler->isExpired('foo')); } @@ -256,7 +256,7 @@ public function testCompileCompilesAndGetThePath(): void public function testCompileSetAndGetThePath(): void { - $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); + $compiler = new BladeCompiler($this->getFiles(), __DIR__); $compiler->setPath('foo'); $this->assertSame('foo', $compiler->getPath()); } @@ -570,7 +570,7 @@ public function testIncludesZeroPath(): void public function testShouldStartFromStrictTypesDeclaration(): void { - $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); + $compiler = new BladeCompiler($this->getFiles(), __DIR__); $strictTypeDecl = "assertSame(substr( $compiler->compileString("assertSame('taylor', $variables['hello']('taylor')); } + public function testDeferredMethodValuesCanBeEscaped(): void + { + $component = new class extends Component { + /** + * Get the view that represents the component. + */ + public function render(): string + { + return 'test'; + } + + /** + * Get the vote count. + */ + public function votes(): int + { + return 42; + } + + /** + * Get the optional label. + */ + public function label(): ?string + { + return null; + } + }; + + $variables = $component->data(); + + $this->assertSame('42', e($variables['votes'])); + $this->assertSame('', e($variables['label'])); + } + public function testIgnoredMethodsAreNotExposedToViewData(): void { $component = new class extends Component { @@ -259,16 +293,3 @@ protected function world($string = 'world') return $string; } } - -class TestDefaultAttributesComponent extends Component -{ - public function __construct() - { - $this->withAttributes(['class' => 'text-red-500']); - } - - public function render(): ViewContract|Htmlable|Closure|string - { - return $this->attributes->get('id'); - } -} diff --git a/types/Database/Eloquent/Builder.php b/types/Database/Eloquent/Builder.php index 2488cb1ae6..37a4891261 100644 --- a/types/Database/Eloquent/Builder.php +++ b/types/Database/Eloquent/Builder.php @@ -191,6 +191,12 @@ function test( assertType('Hypervel\Database\Eloquent\Builder', $query->withAvg('posts', $expression)); assertType('Hypervel\Database\Eloquent\Builder', $query->whereMorphedTo($post->taggable(), new Post)); + assertType('Hypervel\Database\Eloquent\Builder', $query->whereBelongsTo($post)); + assertType('Hypervel\Database\Eloquent\Builder', $query->whereBelongsTo(Post::query()->get())); + assertType('Hypervel\Database\Eloquent\Builder', $query->orWhereBelongsTo(Post::query()->get())); + assertType('Hypervel\Database\Eloquent\Builder', $query->whereAttachedTo($post)); + assertType('Hypervel\Database\Eloquent\Builder', $query->whereAttachedTo(Post::query()->get())); + assertType('Hypervel\Database\Eloquent\Builder', $query->orWhereAttachedTo(Post::query()->get())); assertType('Hypervel\Database\Eloquent\Builder', $query->whereNotMorphedTo($post->taggable(), new Post)); assertType('Hypervel\Database\Eloquent\Builder', $query->orWhereMorphedTo($post->taggable(), new Post)); assertType('Hypervel\Database\Eloquent\Builder', $query->orWhereNotMorphedTo($post->taggable(), new Post)); diff --git a/types/Database/Eloquent/Relations.php b/types/Database/Eloquent/Relations.php index 9c19c21e4c..e9015268f3 100644 --- a/types/Database/Eloquent/Relations.php +++ b/types/Database/Eloquent/Relations.php @@ -109,6 +109,10 @@ function test(User $user, Post $post, Comment $comment, ChildUser $child): void assertType('Hypervel\Contracts\Pagination\Paginator', $user->roles()->simplePaginate()); assertType('Hypervel\Contracts\Pagination\CursorPaginator', $user->roles()->cursorPaginate(cursor: new Cursor(['id' => 1]))); + assertType("Hypervel\\Database\\Eloquent\\Relations\\BelongsToMany", $user->tenantRoles()); + assertType('Hypervel\Database\Eloquent\Collection', $user->tenantRoles()->getResults()); + assertType('Hypervel\Database\Eloquent\Collection', $user->tenantRoles()->get()); + assertType('Hypervel\Database\Eloquent\Relations\HasOneThrough', $user->car()); assertType('Hypervel\Types\Relations\Car|null', $user->car()->getResults()); assertType('Hypervel\Database\Eloquent\Collection', $user->car()->find([1])); @@ -160,6 +164,7 @@ function test(User $user, Post $post, Comment $comment, ChildUser $child): void assertType("Hypervel\\Database\\Eloquent\\Relations\\MorphToMany", $post->tags()); assertType('Hypervel\Database\Eloquent\Collection', $post->tags()->getResults()); + assertType('Hypervel\Database\Eloquent\Collection', $post->tags()->get()); assertType('42', Relation::noConstraints(fn () => 42)); } diff --git a/types/View/InvokableComponentVariable.php b/types/View/InvokableComponentVariable.php new file mode 100644 index 0000000000..ac3f92406a --- /dev/null +++ b/types/View/InvokableComponentVariable.php @@ -0,0 +1,56 @@ + true); + +assertType('Hypervel\View\InvokableComponentVariable', $variable); +assertType('true', $variable()); + +/** + * Verify deferred component variable types. + * + * @param InvokableComponentVariable $flag + * @param InvokableComponentVariable $pair + * @param InvokableComponentVariable $nullable + * @param InvokableComponentVariable $user + * @param InvokableComponentVariable> $users + */ +function test( + InvokableComponentVariable $flag, + InvokableComponentVariable $pair, + InvokableComponentVariable $nullable, + InvokableComponentVariable $untyped, + InvokableComponentVariable $user, + InvokableComponentVariable $users, +): void { + assertType('bool', $flag()); + assertType('array{string, int}', $pair()); + assertType('string|null', $nullable()); + assertType('mixed', $untyped()); + assertType('mixed', $untyped->anything); + + assertType('string', $user->getTable()); + assertType('int<0, max>', $users->count()); + assertType('User|null', $users->first()); + assertType('ArrayIterator', $users->getIterator()); + + acceptsNullableBool($flag); +} + +/** + * Accept a covariant nullable boolean variable. + * + * @param InvokableComponentVariable $variable + */ +function acceptsNullableBool(InvokableComponentVariable $variable): void +{ +}