update project
This commit is contained in:
+66
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 '******';
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user