diff --git a/composer.json b/composer.json index 67ce96c..a73e29b 100644 --- a/composer.json +++ b/composer.json @@ -9,8 +9,7 @@ "ext-imagick": "*", "ext-pdo": "*", "php": "^8.4", - "phpoption/phpoption": "^1.9", - "spatie/image-optimizer": "^1.8", +"spatie/image-optimizer": "^1.8", "staudenmeir/laravel-cte": "^1.0", "intervention/image": "^4.0" }, diff --git a/composer.lock b/composer.lock index 1a7e001..b6bc5de 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b2f189b5c64498c6190267db27e55494", + "content-hash": "a1bf12f1e2b86bc0da8547f2f6944a86", "packages": [ { "name": "brick/math", diff --git a/src/Account/UserRepo.php b/src/Account/UserRepo.php index aa12ea6..b7d6061 100644 --- a/src/Account/UserRepo.php +++ b/src/Account/UserRepo.php @@ -3,7 +3,7 @@ namespace Lucent\Account; use Lucent\Primitive\Collection; -use PhpOption\Option; +use Lucent\Option\Option; interface UserRepo { diff --git a/src/Account/UserRepoLucent.php b/src/Account/UserRepoLucent.php index 0cff2ac..ce4e2fb 100644 --- a/src/Account/UserRepoLucent.php +++ b/src/Account/UserRepoLucent.php @@ -5,7 +5,7 @@ namespace Lucent\Account; use Carbon\Carbon; use Lucent\Database\Database; use Lucent\Primitive\Collection; -use PhpOption\Option; +use Lucent\Option\Option; class UserRepoLucent implements UserRepo { diff --git a/src/Account/UserRepoLunar.php b/src/Account/UserRepoLunar.php index 4266087..9c00f67 100644 --- a/src/Account/UserRepoLunar.php +++ b/src/Account/UserRepoLunar.php @@ -5,7 +5,7 @@ namespace Lucent\Account; use Carbon\Carbon; use Lucent\Database\Database; use Lucent\Primitive\Collection; -use PhpOption\Option; +use Lucent\Option\Option; class UserRepoLunar implements UserRepo { diff --git a/src/Channel/ChannelService.php b/src/Channel/ChannelService.php index 3d18aa8..970d603 100644 --- a/src/Channel/ChannelService.php +++ b/src/Channel/ChannelService.php @@ -8,7 +8,7 @@ use Lucent\File\FileService; use Lucent\Primitive\Collection; use Lucent\Data\Schema; use Lucent\Schema\SchemaService; -use PhpOption\Option; +use Lucent\Option\Option; final class ChannelService { diff --git a/src/Commands/Import.php b/src/Commands/Import.php index 2668ca4..a93b714 100644 --- a/src/Commands/Import.php +++ b/src/Commands/Import.php @@ -4,6 +4,9 @@ namespace Lucent\Commands; use Illuminate\Console\Command; use Lucent\File\FileService; +use Lucent\ResultType\Error; +use Lucent\ResultType\Result; +use Lucent\ResultType\Success; use ZipArchive; class Import extends Command @@ -44,26 +47,44 @@ class Import extends Command return; } - // Extract to temp directory $tempDir = storage_path("exports/.import_tmp_" . uniqid()); + + $result = $this->extractZip($zipFile, $tempDir) + ->flatMap(fn($dir) => $this->restoreDatabase($dir)) + ->flatMap(fn($dir) => $this->restoreFiles($dir, $fileService)); + + $this->cleanup($tempDir); + + if ($result->error()->isDefined()) { + $this->error($result->error()->get()); + return; + } + + $this->info("Import complete."); + } + + /** @return Result */ + private function extractZip(string $zipFile, string $tempDir): Result + { mkdir($tempDir, 0755, true); $zip = new ZipArchive(); if ($zip->open($zipFile) !== true) { - $this->error("Could not open zip archive."); - $this->cleanup($tempDir); - return; + return Error::create("Could not open zip archive."); } $zip->extractTo($tempDir); $zip->close(); - // Restore database + return Success::create($tempDir); + } + + /** @return Result */ + private function restoreDatabase(string $tempDir): Result + { $sqlFiles = glob($tempDir . "/*.sql"); if (empty($sqlFiles)) { - $this->error("No SQL dump found inside the archive."); - $this->cleanup($tempDir); - return; + return Error::create("No SQL dump found inside the archive."); } $db = config("database.connections.pgsql"); @@ -74,7 +95,6 @@ class Import extends Command "lucent_edges", ]; - // Truncate existing tables before restore $truncate = collect($tables) ->map(fn($t) => "TRUNCATE TABLE {$t} CASCADE;") ->join(" "); @@ -90,11 +110,8 @@ class Import extends Command ); exec($truncateCmd, result_code: $truncateCode); - if ($truncateCode !== 0) { - $this->error("Failed to truncate existing tables."); - $this->cleanup($tempDir); - return; + return Error::create("Failed to truncate existing tables."); } $restoreCmd = sprintf( @@ -108,52 +125,49 @@ class Import extends Command ); exec($restoreCmd, result_code: $restoreCode); - if ($restoreCode !== 0) { - $this->error("Database restore failed."); - $this->cleanup($tempDir); - return; + return Error::create("Database restore failed."); } $this->info("Database restored."); - // Replace files + return Success::create($tempDir); + } + + /** @return Result */ + private function restoreFiles(string $tempDir, FileService $fileService): Result + { $srcFilesDir = $tempDir . "/files"; - if (is_dir($srcFilesDir)) { - $publicDisk = $fileService->loadPublicDisk(); - $destFilesDir = $publicDisk->path("lucent/files"); - - // Remove existing files directory or create it if missing - if (is_dir($destFilesDir)) { - exec("rm -rf " . escapeshellarg($destFilesDir)); - } - mkdir($destFilesDir, 0755, true); - - exec( - sprintf( - "cp -R %s/* %s", - escapeshellarg($srcFilesDir), - escapeshellarg($destFilesDir), - ), - result_code: $copyCode, - ); - - if ($copyCode !== 0) { - $this->error("Failed to restore files."); - $this->cleanup($tempDir); - return; - } - - $this->info("Files restored."); - } else { - $this->warn( - "No files directory found in archive — skipping file restore.", - ); + if (!is_dir($srcFilesDir)) { + $this->warn("No files directory found in archive — skipping file restore."); + return Success::create(null); } - $this->cleanup($tempDir); - $this->info("Import complete."); + $publicDisk = $fileService->loadPublicDisk(); + $destFilesDir = $publicDisk->path("lucent/files"); + + if (is_dir($destFilesDir)) { + exec("rm -rf " . escapeshellarg($destFilesDir)); + } + mkdir($destFilesDir, 0755, true); + + exec( + sprintf( + "cp -R %s/* %s", + escapeshellarg($srcFilesDir), + escapeshellarg($destFilesDir), + ), + result_code: $copyCode, + ); + + if ($copyCode !== 0) { + return Error::create("Failed to restore files."); + } + + $this->info("Files restored."); + + return Success::create(null); } private function cleanup(string $dir): void diff --git a/src/Option/LazyOption.php b/src/Option/LazyOption.php new file mode 100644 index 0000000..1b835e3 --- /dev/null +++ b/src/Option/LazyOption.php @@ -0,0 +1,155 @@ + + */ +final class LazyOption extends Option +{ + /** @var callable(mixed...):(Option) */ + private $callback; + + /** @var array */ + private $arguments; + + /** @var Option|null */ + private $option; + + /** + * @template S + * @param callable(mixed...):(Option) $callback + * @param array $arguments + * + * @return LazyOption + */ + public static function create($callback, array $arguments = []): self + { + return new self($callback, $arguments); + } + + /** + * @param callable(mixed...):(Option) $callback + * @param array $arguments + */ + public function __construct($callback, array $arguments = []) + { + if (!is_callable($callback)) { + throw new \InvalidArgumentException('Invalid callback given'); + } + + $this->callback = $callback; + $this->arguments = $arguments; + } + + public function isDefined(): bool + { + return $this->option()->isDefined(); + } + + public function isEmpty(): bool + { + return $this->option()->isEmpty(); + } + + public function get() + { + return $this->option()->get(); + } + + public function getOrElse($default) + { + return $this->option()->getOrElse($default); + } + + public function getOrCall($callable) + { + return $this->option()->getOrCall($callable); + } + + public function getOrThrow(\Exception $ex) + { + return $this->option()->getOrThrow($ex); + } + + public function orElse(Option $else) + { + return $this->option()->orElse($else); + } + + public function ifDefined($callable) + { + $this->option()->forAll($callable); + } + + public function forAll($callable) + { + return $this->option()->forAll($callable); + } + + public function map($callable) + { + return $this->option()->map($callable); + } + + public function flatMap($callable) + { + return $this->option()->flatMap($callable); + } + + public function filter($callable) + { + return $this->option()->filter($callable); + } + + public function filterNot($callable) + { + return $this->option()->filterNot($callable); + } + + public function select($value) + { + return $this->option()->select($value); + } + + public function reject($value) + { + return $this->option()->reject($value); + } + + /** @return Traversable */ + public function getIterator(): Traversable + { + return $this->option()->getIterator(); + } + + public function foldLeft($initialValue, $callable) + { + return $this->option()->foldLeft($initialValue, $callable); + } + + public function foldRight($initialValue, $callable) + { + return $this->option()->foldRight($initialValue, $callable); + } + + /** @return Option */ + private function option(): Option + { + if (null === $this->option) { + /** @var mixed */ + $option = call_user_func_array($this->callback, $this->arguments); + if ($option instanceof Option) { + $this->option = $option; + } else { + throw new \RuntimeException(sprintf('Expected instance of %s', Option::class)); + } + } + + return $this->option; + } +} diff --git a/src/Option/None.php b/src/Option/None.php new file mode 100644 index 0000000..244ee40 --- /dev/null +++ b/src/Option/None.php @@ -0,0 +1,118 @@ + + */ +final class None extends Option +{ + /** @var None|null */ + private static $instance; + + /** @return None */ + public static function create(): self + { + if (null === self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + public function get() + { + throw new \RuntimeException('None has no value.'); + } + + public function getOrCall($callable) + { + return $callable(); + } + + public function getOrElse($default) + { + return $default; + } + + public function getOrThrow(\Exception $ex) + { + throw $ex; + } + + public function isEmpty(): bool + { + return true; + } + + public function isDefined(): bool + { + return false; + } + + public function orElse(Option $else) + { + return $else; + } + + public function ifDefined($callable) + { + // no-op + } + + public function forAll($callable) + { + return $this; + } + + public function map($callable) + { + return $this; + } + + public function flatMap($callable) + { + return $this; + } + + public function filter($callable) + { + return $this; + } + + public function filterNot($callable) + { + return $this; + } + + public function select($value) + { + return $this; + } + + public function reject($value) + { + return $this; + } + + public function getIterator(): EmptyIterator + { + return new EmptyIterator(); + } + + public function foldLeft($initialValue, $callable) + { + return $initialValue; + } + + public function foldRight($initialValue, $callable) + { + return $initialValue; + } + + private function __construct() + { + } +} diff --git a/src/Option/Option.php b/src/Option/Option.php new file mode 100644 index 0000000..3f07905 --- /dev/null +++ b/src/Option/Option.php @@ -0,0 +1,230 @@ + + */ +abstract class Option implements IteratorAggregate +{ + /** + * @template S + * + * @param S $value + * @param S $noneValue + * + * @return Option + */ + public static function fromValue($value, $noneValue = null) + { + if ($value === $noneValue) { + return None::create(); + } + + return new Some($value); + } + + /** + * @template S + * + * @param array|ArrayAccess|null $array + * @param string|int|null $key + * + * @return Option + */ + public static function fromArraysValue($array, $key) + { + if ($key === null || !(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) { + return None::create(); + } + + return new Some($array[$key]); + } + + /** + * @template S + * + * @param callable $callback + * @param array $arguments + * @param S $noneValue + * + * @return LazyOption + */ + public static function fromReturn($callback, array $arguments = [], $noneValue = null) + { + return new LazyOption(static function () use ($callback, $arguments, $noneValue) { + /** @var mixed */ + $return = call_user_func_array($callback, $arguments); + + if ($return === $noneValue) { + return None::create(); + } + + return new Some($return); + }); + } + + /** + * @template S + * + * @param Option|callable|S $value + * @param S $noneValue + * + * @return Option|LazyOption + */ + public static function ensure($value, $noneValue = null) + { + if ($value instanceof self) { + return $value; + } elseif (is_callable($value)) { + return new LazyOption(static function () use ($value, $noneValue) { + /** @var mixed */ + $return = $value(); + + if ($return instanceof self) { + return $return; + } else { + return self::fromValue($return, $noneValue); + } + }); + } else { + return self::fromValue($value, $noneValue); + } + } + + /** + * @template S + * + * @param callable $callback + * @param mixed $noneValue + * + * @return callable + */ + public static function lift($callback, $noneValue = null) + { + return static function () use ($callback, $noneValue) { + /** @var array */ + $args = func_get_args(); + + $reduced_args = array_reduce( + $args, + /** @param bool $status */ + static function ($status, self $o) { + return $o->isEmpty() ? true : $status; + }, + false + ); + + if ($reduced_args) { + return None::create(); + } + + $args = array_map( + static function (self $o) { + return $o->get(); + }, + $args + ); + + return self::ensure(call_user_func_array($callback, $args), $noneValue); + }; + } + + /** @return T */ + abstract public function get(); + + /** + * @template S + * @param S $default + * @return T|S + */ + abstract public function getOrElse($default); + + /** + * @template S + * @param callable():S $callable + * @return T|S + */ + abstract public function getOrCall($callable); + + /** @return T */ + abstract public function getOrThrow(\Exception $ex); + + abstract public function isEmpty(): bool; + + abstract public function isDefined(): bool; + + /** + * @param Option $else + * @return Option + */ + abstract public function orElse(self $else); + + /** @deprecated Use forAll() instead. */ + abstract public function ifDefined($callable); + + /** + * @param callable(T):mixed $callable + * @return Option + */ + abstract public function forAll($callable); + + /** + * @template S + * @param callable(T):S $callable + * @return Option + */ + abstract public function map($callable); + + /** + * @template S + * @param callable(T):Option $callable + * @return Option + */ + abstract public function flatMap($callable); + + /** + * @param callable(T):bool $callable + * @return Option + */ + abstract public function filter($callable); + + /** + * @param callable(T):bool $callable + * @return Option + */ + abstract public function filterNot($callable); + + /** + * @param T $value + * @return Option + */ + abstract public function select($value); + + /** + * @param T $value + * @return Option + */ + abstract public function reject($value); + + /** + * @template S + * @param S $initialValue + * @param callable(S, T):S $callable + * @return S + */ + abstract public function foldLeft($initialValue, $callable); + + /** + * @template S + * @param S $initialValue + * @param callable(T, S):S $callable + * @return S + */ + abstract public function foldRight($initialValue, $callable); +} diff --git a/src/Option/Some.php b/src/Option/Some.php new file mode 100644 index 0000000..b8af29e --- /dev/null +++ b/src/Option/Some.php @@ -0,0 +1,147 @@ + + */ +final class Some extends Option +{ + /** @var T */ + private $value; + + /** @param T $value */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * @template U + * @param U $value + * @return Some + */ + public static function create($value): self + { + return new self($value); + } + + public function isDefined(): bool + { + return true; + } + + public function isEmpty(): bool + { + return false; + } + + public function get() + { + return $this->value; + } + + public function getOrElse($default) + { + return $this->value; + } + + public function getOrCall($callable) + { + return $this->value; + } + + public function getOrThrow(\Exception $ex) + { + return $this->value; + } + + public function orElse(Option $else) + { + return $this; + } + + public function ifDefined($callable) + { + $this->forAll($callable); + } + + public function forAll($callable) + { + $callable($this->value); + + return $this; + } + + public function map($callable) + { + return new self($callable($this->value)); + } + + public function flatMap($callable) + { + /** @var mixed */ + $rs = $callable($this->value); + if (!$rs instanceof Option) { + throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?'); + } + + return $rs; + } + + public function filter($callable) + { + if (true === $callable($this->value)) { + return $this; + } + + return None::create(); + } + + public function filterNot($callable) + { + if (false === $callable($this->value)) { + return $this; + } + + return None::create(); + } + + public function select($value) + { + if ($this->value === $value) { + return $this; + } + + return None::create(); + } + + public function reject($value) + { + if ($this->value === $value) { + return None::create(); + } + + return $this; + } + + /** @return ArrayIterator */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator([$this->value]); + } + + public function foldLeft($initialValue, $callable) + { + return $callable($initialValue, $this->value); + } + + public function foldRight($initialValue, $callable) + { + return $callable($this->value, $initialValue); + } +} diff --git a/src/ResultType/Error.php b/src/ResultType/Error.php new file mode 100644 index 0000000..310df3a --- /dev/null +++ b/src/ResultType/Error.php @@ -0,0 +1,112 @@ + + */ +final class Error extends Result +{ + /** + * @var E + */ + private $value; + + /** + * Internal constructor for an error value. + * + * @param E $value + * + * @return void + */ + private function __construct($value) + { + $this->value = $value; + } + + /** + * Create a new error value. + * + * @template F + * + * @param F $value + * + * @return \Lucent\ResultType\Result + */ + public static function create($value): Error + { + return new self($value); + } + + /** + * Get the success option value. + * + * @return \Lucent\Option\Option + */ + public function success() + { + return None::create(); + } + + /** + * Map over the success value. + * + * @template S + * + * @param callable(T):S $f + * + * @return \Lucent\ResultType\Result + */ + public function map(callable $f): Result + { + return self::create($this->value); + } + + /** + * Flat map over the success value. + * + * @template S + * @template F + * + * @param callable(T):\Lucent\ResultType\Result $f + * + * @return \Lucent\ResultType\Result + */ + public function flatMap(callable $f): Result + { + /** @var \Lucent\ResultType\Result */ + return self::create($this->value); + } + + /** + * Get the error option value. + * + * @return \Lucent\Option\Option + */ + public function error(): Some + { + return Some::create($this->value); + } + + /** + * Map over the error value. + * + * @template F + * + * @param callable(E):F $f + * + * @return \Lucent\ResultType\Result + */ + public function mapError(callable $f): Result + { + return self::create($f($this->value)); + } +} diff --git a/src/ResultType/Result.php b/src/ResultType/Result.php new file mode 100644 index 0000000..a278fa2 --- /dev/null +++ b/src/ResultType/Result.php @@ -0,0 +1,60 @@ + + */ + abstract public function success(); + + /** + * Map over the success value. + * + * @template S + * + * @param callable(T):S $f + * + * @return \Lucent\ResultType\Result + */ + abstract public function map(callable $f); + + /** + * Flat map over the success value. + * + * @template S + * @template F + * + * @param callable(T):\Lucent\ResultType\Result $f + * + * @return \Lucent\ResultType\Result + */ + abstract public function flatMap(callable $f); + + /** + * Get the error option value. + * + * @return \Lucent\Option\Option + */ + abstract public function error(); + + /** + * Map over the error value. + * + * @template F + * + * @param callable(E):F $f + * + * @return \Lucent\ResultType\Result + */ + abstract public function mapError(callable $f); +} diff --git a/src/ResultType/Success.php b/src/ResultType/Success.php new file mode 100644 index 0000000..cfa42e0 --- /dev/null +++ b/src/ResultType/Success.php @@ -0,0 +1,111 @@ + + */ +final class Success extends Result +{ + /** + * @var T + */ + private $value; + + /** + * Internal constructor for a success value. + * + * @param T $value + * + * @return void + */ + private function __construct($value) + { + $this->value = $value; + } + + /** + * Create a new error value. + * + * @template S + * + * @param S $value + * + * @return \Lucent\ResultType\Result + */ + public static function create($value): Success + { + return new self($value); + } + + /** + * Get the success option value. + * + * @return \Lucent\Option\Option + */ + public function success(): Some + { + return Some::create($this->value); + } + + /** + * Map over the success value. + * + * @template S + * + * @param callable(T):S $f + * + * @return \Lucent\ResultType\Result + */ + public function map(callable $f): Result + { + return self::create($f($this->value)); + } + + /** + * Flat map over the success value. + * + * @template S + * @template F + * + * @param callable(T):\Lucent\ResultType\Result $f + * + * @return \Lucent\ResultType\Result + */ + public function flatMap(callable $f) + { + return $f($this->value); + } + + /** + * Get the error option value. + * + * @return \Lucent\Option\Option + */ + public function error() + { + return None::create(); + } + + /** + * Map over the error value. + * + * @template F + * + * @param callable(E):F $f + * + * @return \Lucent\ResultType\Result + */ + public function mapError(callable $f): Result + { + return self::create($this->value); + } +} diff --git a/src/Revision/RevisionRepo.php b/src/Revision/RevisionRepo.php index 1079655..3d521e3 100644 --- a/src/Revision/RevisionRepo.php +++ b/src/Revision/RevisionRepo.php @@ -7,7 +7,7 @@ use Lucent\Database\Database; use Lucent\Edge\Edge; use Lucent\Primitive\Collection; use Lucent\Record\RecordData; -use PhpOption\Option; +use Lucent\Option\Option; use stdClass; class RevisionRepo diff --git a/src/Revision/RevisionService.php b/src/Revision/RevisionService.php index 2d83749..8a997e9 100644 --- a/src/Revision/RevisionService.php +++ b/src/Revision/RevisionService.php @@ -6,7 +6,7 @@ use Lucent\Channel\ChannelService; use Lucent\Edge\Edge; use Lucent\Primitive\Collection; use Lucent\Record\Record; -use PhpOption\Option; +use Lucent\Option\Option; readonly class RevisionService { diff --git a/src/macros.php b/src/macros.php index dad40f0..93188f2 100644 --- a/src/macros.php +++ b/src/macros.php @@ -1,7 +1,7 @@