update project

This commit is contained in:
root
2026-05-30 01:11:35 -04:00
parent 3a0628ecc7
commit 2225f6bc72
9743 changed files with 1122482 additions and 59 deletions
+66
View File
@@ -0,0 +1,66 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "orchestra/sidekick",
"description": "Packages Toolkit Utilities and Helpers for Laravel",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Mior Muhammad Zaki",
"email": "crynobone@gmail.com"
}
],
"autoload": {
"psr-4": {
"Orchestra\\Sidekick\\": "src/"
},
"files": [
"src/Eloquent/functions.php",
"src/Filesystem/functions.php",
"src/Http/functions.php",
"src/functions.php"
]
},
"autoload-dev": {
"psr-4": {
"Orchestra\\Sidekick\\Tests\\": "tests/",
"App\\": "workbench/app/",
"Database\\Factories\\": "workbench/database/factories/"
}
},
"require": {
"php": "^8.1",
"composer-runtime-api": "^2.2",
"composer/semver": "^3.0",
"symfony/polyfill-php83": "^1.32"
},
"require-dev": {
"fakerphp/faker": "^1.21",
"laravel/framework": "^10.48.29|^11.44.7|^12.1.1|^13.0",
"laravel/pint": "^1.4",
"mockery/mockery": "^1.5.1",
"orchestra/testbench-core": "^8.37.0|^9.14.0|^10.2.0|^11.0",
"phpstan/phpstan": "^2.1.14",
"phpunit/phpunit": "^10.0|^11.0|^12.0",
"symfony/process": "^6.0|^7.0"
},
"config": {
"audit": {
"block-insecure": false
},
"sort-packages": true
},
"scripts": {
"lint": [
"@php vendor/bin/pint",
"@php vendor/bin/phpstan analyse --verbose"
],
"test": "@php vendor/bin/phpunit",
"ci": [
"@lint",
"@test"
]
},
"minimum-stability": "dev",
"prefer-stable": true
}
@@ -0,0 +1,59 @@
<?php
namespace Orchestra\Sidekick\Eloquent\Concerns;
use function Orchestra\Sidekick\laravel_version_compare;
/**
* Polyfill for Eloquent Model to get previous attributes.
*
* @see https://github.com/laravel/framework/pull/55729
*
* @codeCoverageIgnore
*/
if (laravel_version_compare('12.15.0', '>=')) {
trait HasPreviousAttributes
{
// ...
}
} else {
trait HasPreviousAttributes
{
/**
* The previous state of the changed model attributes.
*
* @var array<string, mixed>
*/
protected $previous = [];
/** {@inheritDoc} */
#[\Override]
public function syncChanges()
{
parent::syncChanges();
$this->previous = array_intersect_key($this->getRawOriginal(), $this->changes);
return $this;
}
/** {@inheritDoc} */
#[\Override]
public function discardChanges()
{
$this->previous = [];
return parent::discardChanges();
}
/**
* Get the attributes that were previously original before the model was last saved.
*
* @return array<string, mixed>
*/
public function getPrevious()
{
return $this->previous;
}
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace Orchestra\Sidekick\Eloquent;
use Illuminate\Database\Eloquent\Model;
use WeakMap;
/**
* @internal
*/
class Watcher
{
/**
* The cache instance.
*
* @var \WeakMap<\Illuminate\Database\Eloquent\Model, array<string, mixed>>|null
*/
protected static ?WeakMap $cache = null;
/**
* Get the watcher store.
*
* @return \WeakMap<\Illuminate\Database\Eloquent\Model, array<string, mixed>>
*/
public static function store(): WeakMap
{
/** @phpstan-ignore assign.propertyType,return.type */
return static::$cache ??= new WeakMap;
}
/**
* Submit a model snapshot.
*
* @return array<string, mixed>
*/
public static function snapshot(Model $model): ?array
{
$original = $model->getRawOriginal();
$response = match (true) {
$model->isDirty() => $original,
isset(static::store()[$model]) => static::store()[$model],
default => null,
};
static::store()[$model] = $original;
return $response;
}
/**
* Flush the instance states.
*/
public static function flushState(): void
{
static::$cache = null;
}
}
+303
View File
@@ -0,0 +1,303 @@
<?php
namespace Orchestra\Sidekick\Eloquent;
use BackedEnum;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use JsonSerializable;
use Orchestra\Sidekick\SensitiveValue;
use Stringable;
use Throwable;
if (! \function_exists('Orchestra\Sidekick\Eloquent\column_name')) {
/**
* Get qualify column name from Eloquent model.
*
* @api
*
* @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model> $model
*
* @throws \InvalidArgumentException
*/
function column_name(Model|string $model, string $attribute): string
{
if (\is_string($model)) {
$model = new $model;
}
if (! $model instanceof Model) {
throw new InvalidArgumentException(\sprintf('Given $model is not an instance of [%s].', Model::class));
}
return $model->qualifyColumn($attribute);
}
}
if (! \function_exists('Orchestra\Sidekick\Eloquent\is_pivot_model')) {
/**
* Determine if the given model is a pivot model.
*
* @api
*
* @template TPivotModel of (\Illuminate\Database\Eloquent\Model&\Illuminate\Database\Eloquent\Relations\Concerns\AsPivot)|\Illuminate\Database\Eloquent\Relations\Pivot
*
* @param TPivotModel|class-string<TPivotModel> $model
*
* @throws \InvalidArgumentException
*/
function is_pivot_model(Pivot|Model|string $model): bool
{
if (\is_string($model)) {
$model = new $model;
}
if (! $model instanceof Model) {
throw new InvalidArgumentException(\sprintf('Given $model is not an instance of [%s|%s].', Model::class, Pivot::class));
}
if ($model instanceof Pivot) {
return true;
}
return \in_array(AsPivot::class, class_uses_recursive($model), true);
}
}
if (! \function_exists('Orchestra\Sidekick\Eloquent\model_exists')) {
/**
* Check whether given $model exists.
*
* @api
*/
function model_exists(mixed $model): bool
{
return $model instanceof Model && $model->exists === true;
}
}
if (! \function_exists('Orchestra\Sidekick\Eloquent\model_from')) {
/**
* Check whether given $model exists.
*
* @api
*
* @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model> $model
* @param array<string, mixed> $attributes
*/
function model_from(Model|string $model, array $attributes): Model
{
return $model::unguarded(function () use ($model, $attributes) {
if (\is_string($model)) {
$model = new $model;
}
return $model->newInstance()->setRawAttributes($attributes);
});
}
}
if (! \function_exists('Orchestra\Sidekick\Eloquent\model_key_type')) {
/**
* Check whether given $model key type.
*
* @api
*
* @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model> $model
*
* @throws \InvalidArgumentException
*/
function model_key_type(Model|string $model): string
{
if (\is_string($model)) {
$model = new $model;
}
if (! $model instanceof Model) {
throw new InvalidArgumentException(\sprintf('Given $model is not an instance of [%s].', Model::class));
}
$uses = class_uses_recursive($model);
if (\in_array(HasUlids::class, $uses, true)) {
return 'ulid';
} elseif (\in_array(HasUuids::class, $uses, true)) {
return 'uuid';
}
return $model->getKeyType();
}
}
if (! \function_exists('Orchestra\Sidekick\Eloquent\model_diff')) {
/**
* Get attributes diff state from a model.
*
* @api
*
* @param array<int, string> $excludes
* @return array<string, mixed>
*/
function model_diff(Model $model, array $excludes = [], bool $withTimestamps = true): array
{
$hiddens = $model->getHidden();
$excludedAttributes = array_merge($excludes, $model->getAppends());
$timestamps = [$model->getCreatedAtColumn(), $model->getUpdatedAtColumn()];
if (! model_exists($model) || $model->wasRecentlyCreated === true) {
$rawChanges = $model->getAttributes();
$changes = model_from($model, $rawChanges)->setHidden($excludedAttributes)->attributesToArray();
return Arr::except(
summarize_changes($changes, hiddens: $hiddens),
$withTimestamps === false ? $timestamps : [$model->getUpdatedAtColumn()]
);
}
$rawChanges = $model->isDirty() ? $model->getDirty() : $model->getChanges();
$changes = array_intersect_key(
model_from($model, $rawChanges)->setHidden($excludedAttributes)->setAppends([])->attributesToArray(),
$rawChanges
);
return Arr::except(
summarize_changes($changes, hiddens: $hiddens),
$withTimestamps === false ? $timestamps : []
);
}
}
if (! \function_exists('Orchestra\Sidekick\Eloquent\model_snapshot')) {
/**
* Store a snapshot for a model and return the original attributes.
*
* @api
*
* @return array<string, mixed>|null
*/
function model_snapshot(Model $model): ?array
{
return Watcher::snapshot($model);
}
}
if (! \function_exists('Orchestra\Sidekick\Eloquent\model_state')) {
/**
* Get attributes original and changed state from a model.
*
* @api
*
* @param array<int, string> $excludes
* @return array{0: array<string, mixed>|null, 1: array<string, mixed>}
*/
function model_state(Model $model, array $excludes = [], bool $withTimestamps = true): array
{
$changes = model_diff($model, $excludes, $withTimestamps);
$excludedAttributes = array_merge($excludes, $model->getAppends());
if (! model_exists($model) || $model->wasRecentlyCreated == true) {
return [null, $changes];
}
/** @phpstan-ignore function.alreadyNarrowedType */
$previous = method_exists($model, 'getPrevious') ? $model->getPrevious() : null;
if (empty($previous)) {
$previous = Watcher::snapshot($model);
}
$original = summarize_changes(
array_intersect_key(model_from($model, $previous ?? [])->setHidden($excludedAttributes)->setAppends([])->attributesToArray(), $changes),
hiddens: $model->getHidden(),
);
return [$original, $changes];
}
}
if (! \function_exists('Orchestra\Sidekick\Eloquent\normalize_value')) {
/**
* Normalize the given value to be store to database as scalar.
*
* @api
*
* @return scalar
*/
function normalize_value(mixed $value): mixed
{
$value = match (true) {
$value instanceof CarbonInterface => $value->toISOString(),
$value instanceof JsonSerializable => $value->jsonSerialize(),
$value instanceof BackedEnum => $value->value,
default => $value,
};
if (\is_object($value) && $value instanceof Stringable) {
return (string) $value;
} elseif (\is_object($value) || \is_array($value)) {
try {
return json_encode($value);
} catch (Throwable $e) { // @phpstan-ignore catch.neverThrown
return $value;
}
}
return $value;
}
}
if (! \function_exists('Orchestra\Sidekick\Eloquent\summarize_changes')) {
/**
* Get table name from Eloquent model.
*
* @api
*
* @param array<string, mixed> $changes
* @param array<int, string> $hiddens
* @return array<string, \Orchestra\Sidekick\SensitiveValue<mixed>|scalar>
*/
function summarize_changes(array $changes, array $hiddens = []): array
{
$summaries = [];
foreach ($changes as $attribute => $value) {
$summaries[$attribute] = \in_array($attribute, $hiddens, true)
? new SensitiveValue($value)
: normalize_value($value);
}
return $summaries;
}
}
if (! \function_exists('Orchestra\Sidekick\Eloquent\table_name')) {
/**
* Get table name from Eloquent model.
*
* @api
*
* @param \Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model> $model
*
* @throws \InvalidArgumentException
*/
function table_name(Model|string $model): string
{
if (\is_string($model)) {
$model = new $model;
}
if (! $model instanceof Model) {
throw new InvalidArgumentException(\sprintf('Given $model is not an instance of [%s].', Model::class));
}
return $model->getTable();
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace Orchestra\Sidekick;
/**
* @api
*/
class Env extends \Illuminate\Support\Env
{
/**
* Determine if environmemt variable is available.
*/
public static function has(string $key): bool
{
return static::get($key, new UndefinedValue) instanceof UndefinedValue === false;
}
/**
* Set an environment value.
*/
public static function set(string $key, string $value): void
{
static::getRepository()->set($key, $value);
}
/**
* Forget an environment variable.
*
*
* @throws \InvalidArgumentException
*/
public static function forget(string $key): bool
{
return static::getRepository()->clear($key);
}
/**
* Forward environment value.
*
* @param \Orchestra\Sidekick\UndefinedValue|mixed|null $default
* @return mixed
*/
public static function forward(string $key, $default = new UndefinedValue)
{
$value = static::get($key, $default);
if ($value instanceof UndefinedValue) {
return false;
}
return static::encode($value);
}
/**
* Encode environment variable value.
*
* @param mixed $value
* @return mixed
*/
public static function encode($value)
{
if (\is_null($value)) {
return '(null)';
}
if (\is_bool($value)) {
return $value === true ? '(true)' : '(false)';
}
if (empty($value)) {
return '(empty)';
}
return $value;
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace Orchestra\Sidekick\Filesystem;
use ReflectionClass;
if (! \function_exists('Orchestra\Sidekick\Filesystem\filename_from_classname')) {
/**
* Resolve filename from classname.
*
* @api
*
* @param class-string $className
*/
function filename_from_classname(string $className): string|false
{
if (! class_exists($className, false)) {
return false;
}
$classFileName = (new ReflectionClass($className))->getFileName();
if (
$classFileName === false
|| (! is_file($classFileName) && ! str_ends_with(strtolower($classFileName), '.php'))
) {
return false;
}
return realpath($classFileName);
}
}
if (! \function_exists('Orchestra\Sidekick\Filesystem\is_symlink')) {
/**
* Determine if the path is a symlink for both Unix and Windows environments.
*
* @api
*/
function is_symlink(string $path): bool
{
if (\Orchestra\Sidekick\windows_os() && is_dir($path) && readlink($path) !== $path) {
return true;
} elseif (is_link($path)) {
return true;
}
return false;
}
}
if (! \function_exists('Orchestra\Sidekick\Filesystem\join_paths')) {
/**
* Join the given paths together.
*
* @api
*/
function join_paths(?string $basePath, string ...$paths): string
{
foreach ($paths as $index => $path) {
if (empty($path) && $path !== '0') {
unset($paths[$index]);
} else {
$paths[$index] = DIRECTORY_SEPARATOR.ltrim($path, DIRECTORY_SEPARATOR);
}
}
return $basePath.implode('', $paths);
}
}
+176
View File
@@ -0,0 +1,176 @@
<?php
namespace Orchestra\Sidekick;
use ArrayAccess;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Support\Fluent;
use Illuminate\Support\Traits\ForwardsCalls;
use JsonSerializable;
/**
* @api
*
* @template TKey of array-key
* @template TValue
*
* @implements \Illuminate\Contracts\Support\Arrayable<TKey, TValue>
* @implements \ArrayAccess<TKey, TValue>
*/
abstract class FluentDecorator implements Arrayable, ArrayAccess, Jsonable, JsonSerializable
{
use ForwardsCalls;
/**
* The Fluent instance.
*
* @var \Illuminate\Support\Fluent<TKey, TValue>
*/
protected Fluent $fluent;
/**
* Create a new fluent instance.
*
* @param iterable<TKey, TValue> $attributes
*/
public function __construct($attributes = [])
{
$this->fluent = new Fluent($attributes);
}
/**
* Convert the fluent instance to an array.
*
* @return array<TKey, TValue>
*/
public function toArray()
{
return $this->fluent->getAttributes();
}
/**
* Convert the object into something JSON serializable.
*
* @return array<TKey, TValue>
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* Convert the fluent instance to JSON.
*
* @param int $options
* @return string
*/
public function toJson($options = 0)
{
return (string) json_encode($this->jsonSerialize(), $options);
}
/**
* Determine if the given offset exists.
*
* @param TKey $offset
*/
public function offsetExists($offset): bool
{
return $this->fluent->offsetExists($offset);
}
/**
* Get the value for a given offset.
*
* @param TKey $offset
* @return TValue|null
*/
public function offsetGet($offset): mixed
{
return $this->fluent->offsetGet($offset);
}
/**
* Set the value at the given offset.
*
* @param TKey $offset
* @param TValue $value
*/
public function offsetSet($offset, $value): void
{
$this->fluent->offsetSet($offset, $value);
}
/**
* Unset the value at the given offset.
*
* @param TKey $offset
*/
public function offsetUnset($offset): void
{
$this->fluent->offsetUnset($offset);
}
/**
* Handle dynamic calls to the fluent instance to set attributes.
*
* @param TKey $method
* @param array{0: ?TValue} $parameters
* @return $this
*/
public function __call($method, $parameters)
{
return $this->forwardDecoratedCallTo($this->fluent, $method, $parameters);
}
/**
* Dynamically retrieve the value of an attribute.
*
* @param TKey $key
* @return TValue|null
*/
public function __get($key)
{
/** @phpstan-ignore function.alreadyNarrowedType */
if (method_exists($this->fluent, 'value')) {
return $this->fluent->value($key);
}
return $this->fluent->get($key);
}
/**
* Dynamically set the value of an attribute.
*
* @param TKey $key
* @param TValue $value
* @return void
*/
public function __set($key, $value)
{
$this->fluent->offsetSet($key, $value);
}
/**
* Dynamically check if an attribute is set.
*
* @param TKey $key
* @return bool
*/
public function __isset($key)
{
return $this->fluent->offsetExists($key);
}
/**
* Dynamically unset an attribute.
*
* @param TKey $key
* @return void
*/
public function __unset($key)
{
$this->fluent->offsetUnset($key);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Orchestra\Sidekick\Http;
if (! \function_exists('Orchestra\Sidekick\Http\safe_int')) {
/**
* Convert large id higher than Number.MAX_SAFE_INTEGER to string.
*
* https://stackoverflow.com/questions/47188449/json-max-int-number/47188576
*
* @api
*/
function safe_int(mixed $value): mixed
{
$jsonMaxInt = 9007199254740991;
if (\is_int($value) && abs($value) >= $jsonMaxInt) {
return (string) $value;
} elseif (filter_var($value, FILTER_VALIDATE_INT) && abs($value) < $jsonMaxInt) {
return (int) $value;
}
return $value;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Orchestra\Sidekick;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\PhpExecutableFinder as SymfonyPhpExecutableFinder;
/**
* @internal
*/
class PhpExecutableFinder extends SymfonyPhpExecutableFinder
{
/**
* {@inheritDoc}
*
* @codeCoverageIgnore
*/
#[\Override]
public function find(bool $includeArgs = true): string|false
{
if ($herdPath = getenv('HERD_HOME')) {
return (new ExecutableFinder)->find(name: 'php', extraDirs: [join_paths($herdPath, 'bin')]) ?? false;
}
return parent::find($includeArgs);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace Orchestra\Sidekick;
use JsonSerializable;
use Stringable;
/**
* @api
*
* @template TValue of mixed
*/
final class SensitiveValue implements JsonSerializable, Stringable
{
/**
* Construct a new sensitive value.
*
* @param TValue $value
*/
public function __construct(
private readonly mixed $value
) {
//
}
/**
* Get the original value.
*
* @return TValue
*/
public function getValue(): mixed
{
return $this->value;
}
/**
* Transform the value for debugging.
*/
public function __debugInfo(): array
{
return [];
}
/**
* Get the value for string serialization.
*/
public function __toString(): string
{
return $this->jsonSerialize();
}
/**
* Get the value for JSON serialization.
*/
public function jsonSerialize(): string
{
return '******';
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace Orchestra\Sidekick;
use JsonSerializable;
/**
* @api
*/
final class UndefinedValue implements JsonSerializable
{
/**
* Determine if value is equivalent to "undefined" or "null".
*
* @param mixed $value
*
* @phpstan-assert-if-true \Orchestra\Sidekick\UndefinedValue|null $value
*/
public static function equalsTo($value): bool
{
return $value instanceof UndefinedValue || \is_null($value);
}
/**
* Get the value for JSON serialization.
*
* @return null
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return null;
}
}
+415
View File
@@ -0,0 +1,415 @@
<?php
namespace Orchestra\Sidekick;
use BackedEnum;
use Closure;
use Composer\InstalledVersions;
use Composer\Semver\VersionParser;
use Illuminate\Contracts\Foundation\Application as ApplicationContract;
use Illuminate\Foundation\Application;
use Illuminate\Support\Arr;
use OutOfBoundsException;
use PHPUnit\Runner\Version;
use RuntimeException;
use UnitEnum;
if (! \function_exists('Orchestra\Sidekick\enum_name')) {
/**
* Get the proper name from enum.
*
* @api
*
* @throws \RuntimeException
*/
function enum_name(BackedEnum|UnitEnum $enum): string
{
return mb_convert_case(str_replace('_', ' ', $enum->name), MB_CASE_TITLE, 'UTF-8');
}
}
if (! \function_exists('Orchestra\Sidekick\enum_value')) {
/**
* Get the proper name from enum.
*
* @api
*
* @template TValue
* @template TDefault
*
* @param TValue $value
* @param TDefault|callable(TValue): TDefault $default
* @return ($value is empty ? TDefault : mixed)
*
* @throws \RuntimeException
*/
function enum_value(mixed $value, mixed $default = null): mixed
{
return match (true) {
$value instanceof BackedEnum => $value->value,
$value instanceof UnitEnum => $value->name,
default => $value ?? value($default),
};
}
}
if (! \function_exists('Orchestra\Sidekick\after_resolving')) {
/**
* Register after resolving callback.
*
* @api
*
* @template TLaravel of \Illuminate\Contracts\Foundation\Application
*
* @param TLaravel $app
* @param class-string|string $name
* @param (\Closure(object, TLaravel):(mixed))|null $callback
*/
function after_resolving(ApplicationContract $app, string $name, ?Closure $callback = null): void
{
$app->afterResolving($name, $callback);
if ($app->resolved($name)) {
value($callback, $app->make($name), $app);
}
}
}
if (! \function_exists('Orchestra\Sidekick\join_paths')) {
/**
* Join the given paths together.
*
* @api
*
* @deprecated
*/
function join_paths(?string $basePath, string ...$paths): string
{
return Filesystem\join_paths($basePath, ...$paths);
}
}
if (! \function_exists('Orchestra\Sidekick\once')) {
/**
* Run callback only once.
*
* @api
*
* @param mixed $callback
* @return \Closure():mixed
*/
function once($callback): Closure
{
$response = new UndefinedValue;
return function () use ($callback, &$response) {
if ($response instanceof UndefinedValue) {
$response = value($callback) ?? null;
}
return $response;
};
}
}
if (! \function_exists('Orchestra\Sidekick\is_safe_callable')) {
/**
* Determine if the value is a callable and not a string matching an available function name.
*
* @api
*/
function is_safe_callable(mixed $value): bool
{
if ($value instanceof Closure) {
return true;
}
if (! \is_callable($value)) {
return false;
}
if (\is_array($value)) {
return \count($value) === 2 && array_is_list($value) && method_exists(...$value);
}
return ! \is_string($value);
}
}
if (! \function_exists('Orchestra\Sidekick\is_symlink')) {
/**
* Determine if the path is a symlink for both Unix and Windows environments.
*
* @api
*
* @deprecated
*/
function is_symlink(string $path): bool
{
return Filesystem\is_symlink($path);
}
}
if (! \function_exists('Orchestra\Sidekick\is_testbench_cli')) {
/**
* Determine if command executed via Testbench CLI.
*
* @api
*/
function is_testbench_cli(?bool $dusk = null): bool
{
$usingTestbench = \defined('TESTBENCH_CORE');
$usingTestbenchDusk = \defined('TESTBENCH_DUSK');
return match ($dusk) {
false => $usingTestbench === true && $usingTestbenchDusk === false,
true => $usingTestbench === true && $usingTestbenchDusk === true,
default => $usingTestbench === true,
};
}
}
if (! \function_exists('Orchestra\Sidekick\transform_relative_path')) {
/**
* Transform relative path.
*
* @api
*/
function transform_relative_path(string $path, string $workingPath): string
{
return str_starts_with($path, './')
? rtrim($workingPath, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.mb_substr($path, 2)
: $path;
}
}
if (! \function_exists('Orchestra\Sidekick\package_path')) {
/**
* Get the package path.
*
* @api
*
* @no-named-arguments
*
* @param array<int, string|null>|string ...$path
*/
function package_path(array|string $path = ''): string
{
$packagePath = once(function () {
$workingPath = realpath(match (true) {
\defined('TESTBENCH_WORKING_PATH') => TESTBENCH_WORKING_PATH,
Env::has('TESTBENCH_WORKING_PATH') => Env::get('TESTBENCH_WORKING_PATH'),
default => InstalledVersions::getRootPackage()['install_path'],
});
return $workingPath !== false ? $workingPath : getcwd();
});
return join_paths($packagePath(), ...Arr::wrap(\func_num_args() > 1 ? \func_get_args() : $path));
}
}
if (! \function_exists('Orchestra\Sidekick\working_path')) {
/**
* Get the working path.
*
* @api
*
* @no-named-arguments
*
* @param array<int, string|null>|string ...$path
*/
function working_path(array|string $path = ''): string
{
return is_testbench_cli()
? package_path($path)
: base_path(join_paths(...Arr::wrap(\func_num_args() > 1 ? \func_get_args() : $path)));
}
}
if (! \function_exists('Orchestra\Sidekick\laravel_normalize_version')) {
/**
* Laravel normalize version.
*
* @api
*
* @throws \OutOfBoundsException
*/
function laravel_normalize_version(): string
{
if (! class_exists(Application::class)) {
throw new OutOfBoundsException('Unable to verify "laravel/framework" version');
}
/** @var string $version */
$version = transform(
Application::VERSION,
fn (string $version) => match ($version) {
'13.x-dev' => '13.0.0',
default => $version,
}
);
return (new VersionParser)->normalize($version);
}
}
if (! \function_exists('Orchestra\Sidekick\phpunit_normalize_version')) {
/**
* PHPUnit normalize version.
*
* @api
*
* @throws \OutOfBoundsException
*/
function phpunit_normalize_version(): string
{
if (! class_exists(Version::class)) {
throw new OutOfBoundsException('Unable to verify "phpunit/phpunit" version');
}
/** @var string $version */
$version = transform(
Version::id(),
fn (string $version) => match (true) {
str_starts_with($version, '13.0-') => '13.0.0',
default => $version,
}
);
return (new VersionParser)->normalize($version);
}
}
if (! \function_exists('Orchestra\Sidekick\laravel_version_compare')) {
/**
* Laravel version compare.
*
* @api
*
* @template TOperator of string|null
*
* @param TOperator $operator
* @return (TOperator is null ? int : bool)
*
* @throws \RuntimeException
*
* @codeCoverageIgnore
*/
function laravel_version_compare(string $version, ?string $operator = null): int|bool
{
if (! class_exists(Application::class)) {
return package_version_compare('laravel/framework', $version, $operator);
}
$laravel = laravel_normalize_version();
$version = (new VersionParser)->normalize($version);
if (\is_null($operator)) {
return version_compare($laravel, $version);
}
return version_compare($laravel, $version, $operator);
}
}
if (! \function_exists('Orchestra\Sidekick\package_version_compare')) {
/**
* Package version compare.
*
* @api
*
* @template TOperator of string|null
*
* @phpstan-param TOperator $operator
*
* @phpstan-return (TOperator is null ? int : bool)
*
* @throws \OutOfBoundsException
* @throws \RuntimeException
*
* @codeCoverageIgnore
*/
function package_version_compare(string $package, string $version, ?string $operator = null): int|bool
{
$prettyVersion = InstalledVersions::getPrettyVersion($package);
if (\is_null($prettyVersion)) {
throw new RuntimeException(\sprintf('Unable to compare "%s" version', $package));
}
$versionParser = new VersionParser;
$package = $versionParser->normalize($prettyVersion);
$version = $versionParser->normalize($version);
if (\is_null($operator)) {
return version_compare($package, $version);
}
return version_compare($package, $version, $operator);
}
}
if (! \function_exists('Orchestra\Sidekick\phpunit_version_compare')) {
/**
* PHPUnit version compare.
*
* @api
*
* @template TOperator of string|null
*
* @param TOperator $operator
* @return (TOperator is null ? int : bool)
*
* @throws \OutOfBoundsException
* @throws \RuntimeException
*
* @codeCoverageIgnore
*/
function phpunit_version_compare(string $version, ?string $operator = null): int|bool
{
if (! class_exists(Version::class)) {
return package_version_compare('phpunit/phpunit', $version, $operator);
}
$phpunit = phpunit_normalize_version();
$version = (new VersionParser)->normalize($version);
if (\is_null($operator)) {
return version_compare($phpunit, $version);
}
return version_compare($phpunit, $version, $operator);
}
}
if (! \function_exists('Orchestra\Sidekick\php_binary')) {
/**
* Determine the PHP Binary.
*
* @api
*
* @codeCoverageIgnore
*/
function php_binary(): string
{
return (new PhpExecutableFinder)->find(false) ?: 'php';
}
}
if (! \function_exists('Orchestra\Sidekick\windows_os')) {
/**
* Determine whether the current environment is Windows-based.
*
* @api
*
* @codeCoverageIgnore
*/
function windows_os(): bool
{
return PHP_OS_FAMILY === 'Windows';
}
}