diff --git a/Radiate/Console/Parser.php b/Radiate/Console/Parser.php index a187834..5c07087 100644 --- a/Radiate/Console/Parser.php +++ b/Radiate/Console/Parser.php @@ -3,6 +3,7 @@ namespace Radiate\Console; use Exception; +use Radiate\Support\Facades\Str; class Parser { @@ -76,7 +77,7 @@ protected static function parseArgument(string $token): array [$token, $description] = static::extractDescription($token); switch (true) { - case static::endsWith($token, '?'): + case Str::endsWith($token, '?'): return [ 'type' => 'positional', 'name' => trim($token, '?'), @@ -110,7 +111,7 @@ protected static function parseOption(string $token): array [$token, $description] = static::extractDescription($token); switch (true) { - case static::endsWith($token, '='): + case Str::endsWith($token, '='): return [ 'type' => 'assoc', 'name' => trim($token, '='), @@ -145,22 +146,4 @@ protected static function extractDescription(string $token): array return count($parts) === 2 ? $parts : [$token, '']; } - - /** - * Determine if a string ends with a substring - * - * @param string $haystack - * @param string $needle - * @return void - */ - protected static function endsWith(string $haystack, string $needle) - { - $length = strlen($needle); - if ( - !$length - ) { - return true; - } - return substr($haystack, -$length) === $needle; - } } diff --git a/Radiate/Foundation/Application.php b/Radiate/Foundation/Application.php index 2565055..52341a5 100644 --- a/Radiate/Foundation/Application.php +++ b/Radiate/Foundation/Application.php @@ -9,9 +9,9 @@ use Radiate\Foundation\Providers\ConsoleServiceProvider; use Radiate\Filesystem\FilesystemServiceProvider; use Radiate\Http\Request; -use Radiate\Routing\Pipeline; use Radiate\Routing\RoutingServiceProvider; use Radiate\Support\Facades\Facade; +use Radiate\Support\Pipeline; use Radiate\View\ViewServiceProvider; use RuntimeException; use Throwable; diff --git a/Radiate/Mail/MailServiceProvider.php b/Radiate/Mail/MailServiceProvider.php index eadec01..ed8377b 100644 --- a/Radiate/Mail/MailServiceProvider.php +++ b/Radiate/Mail/MailServiceProvider.php @@ -2,7 +2,6 @@ namespace Radiate\Mail; -use Parsedown; use Radiate\Support\ServiceProvider; class MailServiceProvider extends ServiceProvider @@ -17,10 +16,6 @@ public function register(): void $this->app->singleton('mailer', function ($app) { return new Mailer($app['events']); }); - - $this->app->singleton('markdown', function () { - return new Parsedown(); - }); } /** diff --git a/Radiate/Mail/Mailable.php b/Radiate/Mail/Mailable.php index 570022d..c1c3171 100644 --- a/Radiate/Mail/Mailable.php +++ b/Radiate/Mail/Mailable.php @@ -2,7 +2,7 @@ namespace Radiate\Mail; -use Radiate\Support\Facades\App; +use Radiate\Support\Facades\Str; use Radiate\Support\Facades\View; use ReflectionClass; use ReflectionProperty; @@ -195,7 +195,7 @@ public function text(string $path, array $data = []): self */ public function view(string $path, array $data = []): self { - $this->html = View::view($path, $this->buildViewData($data)); + $this->html = View::make($path, $this->buildViewData($data)); return $this; } @@ -211,7 +211,7 @@ public function markdown(string $path, array $data = []): self { $this->text($path, $data); - $this->html = App::get('markdown')->text($this->text); + $this->html = Str::markdown($this->text); return $this; } diff --git a/Radiate/Routing/Route.php b/Radiate/Routing/Route.php index 127423c..c02c1e5 100644 --- a/Radiate/Routing/Route.php +++ b/Radiate/Routing/Route.php @@ -4,6 +4,7 @@ use Radiate\Foundation\Application; use Radiate\Http\Request; +use Radiate\Support\Pipeline; use Throwable; abstract class Route diff --git a/Radiate/Routing/Router.php b/Radiate/Routing/Router.php index ac532e8..0ea00b0 100644 --- a/Radiate/Routing/Router.php +++ b/Radiate/Routing/Router.php @@ -62,7 +62,7 @@ public function __construct(Dispatcher $events, Application $app) * * @param string $uri * @param mixed $action - * @return void + * @return \Radiate\Routing\Route */ public function ajax(string $uri, $action) { @@ -70,11 +70,11 @@ public function ajax(string $uri, $action) } /** - * Create an GET route + * Create a GET route * * @param string $uri * @param mixed $action - * @return void + * @return \Radiate\Routing\Route */ public function get(string $uri, $action) { @@ -86,7 +86,7 @@ public function get(string $uri, $action) * * @param string $uri * @param mixed $action - * @return void + * @return \Radiate\Routing\Route */ public function post(string $uri, $action) { @@ -98,7 +98,7 @@ public function post(string $uri, $action) * * @param string $uri * @param mixed $action - * @return void + * @return \Radiate\Routing\Route */ public function put(string $uri, $action) { @@ -110,7 +110,7 @@ public function put(string $uri, $action) * * @param string $uri * @param mixed $action - * @return void + * @return \Radiate\Routing\Route */ public function patch(string $uri, $action) { @@ -122,7 +122,7 @@ public function patch(string $uri, $action) * * @param string $uri * @param mixed $action - * @return void + * @return \Radiate\Routing\Route */ public function delete(string $uri, $action) { @@ -130,11 +130,11 @@ public function delete(string $uri, $action) } /** - * Create an route with any method + * Create a route with any method * * @param string $uri * @param mixed $action - * @return void + * @return \Radiate\Routing\Route */ public function any(string $uri, $action) { @@ -142,12 +142,12 @@ public function any(string $uri, $action) } /** - * Create an route matching the given methods + * Create a route matching the given methods * * @param array $methods * @param string $uri * @param mixed $action - * @return void + * @return \Radiate\Routing\Route */ public function matches(array $methods, string $uri, $action) { @@ -190,8 +190,8 @@ public function resource(string $uri, string $action, array $methods = ['index', /** * Regsiter the route in the router * - * @param Radiate\Routing\Route $route - * @return void + * @param \Radiate\Routing\Route $route + * @return \Radiate\Routing\Route */ public function addRoute(Route $route) { diff --git a/Radiate/Support/Collection.php b/Radiate/Support/Collection.php new file mode 100644 index 0000000..5a10a85 --- /dev/null +++ b/Radiate/Support/Collection.php @@ -0,0 +1,792 @@ +items = $items; + } + + /** + * Create a collection instance + * + * @param array $items + * @return static + */ + public static function collect(array $items) + { + return new static($items); + } + + /** + * Add an item to the collection. + * + * @param mixed $item + * @return static + */ + public function add($item) + { + $this->items[] = $item; + + return $this; + } + + /** + * Get the collection items + * + * @return array + */ + public function all(): array + { + return $this->items; + } + + /** + * Split a collection into chunks + * + * @param int $size + * @return static + */ + public function chunk(int $size) + { + $results = new static(array_chunk($this->items, $size, true)); + + return $results->map(function ($chunk) { + return new static($chunk); + }); + } + + /** + * Create a collection by using this collection for keys and another for its + * values + * + * @param mixed $values + * @return static + */ + public function combine($values) + { + return new static( + array_combine($this->items, $this->getArrayableItems($values)) + ); + } + + /** + * Get the items in the collection that are not present in the given items + * + * @param mixed $items + * @return static + */ + public function diff($items) + { + return new static( + array_diff($this->items, $this->getArrayableItems($items)) + ); + } + + /** + * Execute a callback over each item + * + * @param callable $callback + * @return static + */ + public function each(callable $callback) + { + foreach ($this->items as $key => $item) { + if ($callback($item, $key) === false) { + break; + } + } + + return $this; + } + + /** + * Get all items except for those with the specified keys. + * + * @param string,... $keys + * @return static + */ + public function except(string ...$keys) + { + $new = clone $this; + + foreach ($keys as $key) { + $new->forget($key); + } + + return $new; + } + + /** + * Iterates over each item in the collection passing them to the callback + * function + * + * @param callable|null $callback + * @return static + */ + public function filter(?callable $callback = null) + { + return new static( + array_filter($this->items, $callback, ARRAY_FILTER_USE_BOTH) + ); + } + + /** + * Gets the first item of the collection + * + * @return mixed + */ + public function first() + { + return $this->items[$this->firstKey()]; + } + + /** + * Gets the first key of the collection + * + * @return string|int|null + */ + public function firstKey() + { + return array_key_first($this->items); + } + + /** + * Flatten a multi-dimensional array into a single level + * + * @return static + */ + public function flatten() + { + $new = clone $this; + + $results = []; + + array_walk_recursive($new->items, function ($value) use (&$results) { + $results[] = $value; + }); + + return new static($results); + } + + /** + * Exchanges all keys with their associated values in a collection + * + * @return static + */ + public function flip() + { + return new static(array_flip($this->items)); + } + + /** + * Unset an item in the collection + * + * @param string $key + * @return static + */ + public function forget(string $key) + { + unset($this->items[$key]); + + return $this; + } + + /** + * Get an item from the collection + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get(string $key, $default) + { + return $this->items[$key] ?? $default; + } + + /** + * Determine if an item in the collection exists + * + * @param string $key + * @return bool + */ + public function has(string $key): bool + { + return isset($this->items[$key]); + } + + /** + * Determine if the given key or index exists in the collection + * + * @param string $key + * @return bool + */ + public function hasKey(string $key): bool + { + return array_key_exists($key, $this->items); + } + + /** + * Intersect the collection with the given items + * + * @param mixed $items + * @return static + */ + public function intersect($items) + { + return new static( + array_intersect($this->items, $this->getArrayableItems($items)) + ); + } + + /** + * Computes the intersection of the collection using keys for comparison + * + * @param mixed $items + * @return static + */ + public function intersectByKeys($items) + { + return new static( + array_intersect_key($this->items, $this->getArrayableItems($items)) + ); + } + + /** + * Determine if the collection is empty or not + * + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->items); + } + + /** + * Determine if the collection is empty or not + * + * @return bool + */ + public function isNotEmpty(): bool + { + return !empty($this->items); + } + + /** + * Join all items from the collection using a string. + * + * @param string $glue + * @return string + */ + public function join(string $glue): string + { + return implode($glue, $this->items); + } + + /** + * Get the keys from the collection + * + * @return static + */ + public function keys() + { + return new static(array_keys($this->items)); + } + + /** + * Gets the last item of the collection + * + * @return mixed + */ + public function last() + { + return $this->items[$this->lastKey()]; + } + + /** + * Gets the last key of the collection + * + * @return string|int|null + */ + public function lastKey() + { + return array_key_last($this->items); + } + + /** + * Applies the callback to the elements of the collection + * + * @param callable $callback + * @return static + */ + public function map(callable $callback) + { + $items = array_map($callback, $this->items, $keys = array_keys($this->items)); + + return new static(array_combine($keys, $items)); + } + + /** + * Merge the collection with the given items + * + * @param mixed $array + * @return static + */ + public function merge($array) + { + return new static( + array_merge($this->items, $this->getArrayableItems($array)) + ); + } + + /** + * Recursively merge the collection with the given items + * + * @param mixed $array + * @return static + */ + public function mergeRecursive($array) + { + return new static( + array_merge_recursive($this->items, $this->getArrayableItems($array)) + ); + } + + /** + * Get the items with the specified keys + * + * @param string,... $keys + * @return static + */ + public function only(string ...$keys) + { + return new static($this->intersectByKeys(array_flip($keys))->all()); + } + + /** + * Pass the collection to the given callback and return the result + * + * @param callable $callback + * @return mixed + */ + public function pipe(callable $callback) + { + return $callback($this); + } + + /** + * Get and remove the last item from the collection + * + * @return mixed + */ + public function pop() + { + return array_pop($this->items); + } + + /** + * Push an item onto the beginning of the collection + * + * @param mixed $value + * @param string|int|null $key + * @return static + */ + public function prepend($value, $key = null) + { + if ($key) { + $this->items = [$key => $value] + $this->items; + } else { + array_unshift($this->items, $value); + } + + return $this; + } + + /** + * Push elements onto the end of the collection + * + * @param mixed,... $values + * @return static + */ + public function push(...$values) + { + foreach ($values as $value) { + $this->items[] = $value; + } + + return $this; + } + + /** + * Push keyed elements onto the end of the collection + * + * @param string $key + * @param mixed $value + * @return static + */ + public function put(string $key, $value) + { + $this->items[$key] = $value; + + return $this; + } + + /** + * Pick one or more random entries out of the collection + * + * @param int $number + * @param bool $preserveKeys + * @return mixed|static + * + * @throws \InvalidArgumentException + */ + public function random(int $number = 1, bool $preserveKeys = false) + { + if ($number == 1) { + return $this->items[array_rand($this->items)]; + } + + if ($number == 0) { + return new static(); + } + + if ($number > $count = $this->count()) { + throw new InvalidArgumentException( + "You requested {$number} items, but there are only {$count} items available." + ); + } + + $results = new static(); + + foreach (array_rand($this->items, $number) as $key) { + if ($preserveKeys) { + $results[$key] = $this->items[$key]; + } else { + $results[] = $this->items[$key]; + } + } + + return $results; + } + + /** + * Reduce the collection to a single value + * + * @param callable $callback + * @param mixed $initial + * @return mixed + */ + public function reduce(callable $callback, $initial = null) + { + return array_reduce($this->items, $callback, $initial); + } + + /** + * Replace the collection items with the given items + * + * @param mixed $items + * @return static + */ + public function replace($items) + { + return new static( + array_replace($this->items, $this->getArrayableItems($items)) + ); + } + + /** + * Recursively replace the collection items with the given items + * + * @param mixed $items + * @return static + */ + public function replaceRecursive($items) + { + return new static( + array_replace_recursive($this->items, $this->getArrayableItems($items)) + ); + } + + /** + * Return a collection with elements in reverse order + * + * @return static + */ + public function reverse() + { + return new static(array_reverse($this->items, true)); + } + + /** + * Searches the collection for a given value and returns the corresponding + * key if successful + * + * @param mixed $value + * @param boolean $strict + * @return int|string|false + */ + public function search($value, bool $strict = false) + { + return array_search($value, $this->items, $strict); + } + + /** + * Shift an element off the beginning of the collection + * + * @return mixed + */ + public function shift() + { + return array_shift($this->items); + } + + /** + * Shuffle the items in the collection + * + * @return static + */ + public function shuffle() + { + $keys = array_keys($this->items); + + $results = new static(); + + shuffle($keys); + + foreach ($keys as $key) { + $results[$key] = $this->items[$key]; + } + + return $results; + } + + /** + * Skip the first x number of items + * + * @param int $count + * @return static + */ + public function skip(int $count) + { + return $this->slice($count); + } + + /** + * Extract a slice of the collection + * + * @param int $offset + * @param int|null $length + * @return static + */ + public function slice(int $offset, ?int $length = null) + { + return new static(array_slice($this->items, $offset, $length, true)); + } + + /** + * Take the first or last x number of items from the collection + * + * @param int $limit + * @return static + */ + public function take(int $limit) + { + return $this->slice($limit < 0 ? $limit : 0, abs($limit)); + } + + /** + * Pass the collection to the given callback and then return it + * + * @param callable $callback + * @return static + */ + public function tap(callable $callback) + { + $callback(clone $this); + + return $this; + } + + /** + * Get the collection of items as an array + * + * @return array + */ + public function toArray(): array + { + return $this->map(function ($value) { + return $value instanceof Collection + ? $value->toArray() + : $value; + })->all(); + } + + /** + * Get the collection of items as JSON + * + * @param int $options + * @return string + */ + public function toJson(int $options = 0): string + { + return json_encode($this->items, $options); + } + + /** + * Return all the values of the collection + * + * @return static + */ + public function values() + { + return new static(array_values($this->items)); + } + + /** + * Return an array of items from Collection or Traversable + * + * @param mixed $items + * @return array + */ + protected function getArrayableItems($items): array + { + if (is_array($items)) { + return $items; + } elseif ($items instanceof Traversable) { + return iterator_to_array($items); + } + + return (array) $items; + } + + /** + * Determine if an item in the collection exists + * + * @param string $key + * @return bool + */ + public function offsetExists($key): bool + { + return isset($this->items[$key]); + } + + /** + * Get an item from the collection + * + * @param string $key + * @return mixed + */ + public function offsetGet($key) + { + return $this->items[$key]; + } + + /** + * Set an item in the collection + * + * @param string|null $key + * @param mixed $value + * @return void + */ + public function offsetSet($key, $value): void + { + if ($key) { + $this->items[$key] = $value; + } else { + $this->items[] = $value; + } + } + + /** + * Unset an item in the collection + * + * @param string $key + * @return void + */ + public function offsetUnset($key): void + { + unset($this->items[$key]); + } + + /** + * Get the collection items count + * + * @return int + */ + public function count(): int + { + return count($this->items); + } + + /** + * Implements \IteratorAggregate + * + * @return \ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->items); + } + + /** + * JSON serialise the collection + * + * @return array + */ + public function jsonSerialize(): array + { + return $this->all(); + } + + /** + * Dynamically get an item from the collection + * + * @param string $key + * @return mixed + */ + public function __get(string $key) + { + return $this->items[$key]; + } + + /** + * Dynamically set items in the collection + * + * @param string $key + * @param mixed $value + * @return void + */ + public function __set(string $key, $value): void + { + $this->items[$key] = $value; + } + + /** + * Return the collection as a JSON string + * + * @return string + */ + public function __toString(): string + { + return $this->toJson(); + } +} diff --git a/Radiate/Support/Facades/App.php b/Radiate/Support/Facades/App.php index 398a04a..95a6d8e 100644 --- a/Radiate/Support/Facades/App.php +++ b/Radiate/Support/Facades/App.php @@ -2,6 +2,31 @@ namespace Radiate\Support\Facades; +/** + * @method static string basePath(string $path = null) Get the app base path + * @method static void register(string $provider) Register a service provider + * @method static \Radiate\Foundation\Application middleware(array $middleware) Add a global middleware to the app + * @method static \Radiate\Foundation\Application routeMiddleware(array $middleware) Add a global middleware to the app + * @method static array getRouteMiddleware() Get the route middleware + * @method static void boot() Boot the application + * @method static string getNamespace() Get the app namespace + * @method static bool runningInConsole() Determine if the app is running in the console + * @method static string renderException(\Radiate\Http\Request $request, \Throwable $e) Render an HTTP exception + * @method static string|bool environment(string|array|null $environments = null) Get or check the current application environment. + * @method static bool isLocal() Determine if the app is in a local environment + * @method static bool isDevelopment() Determine if the app is in a development environment + * @method static bool isStaging() Determine if the app is in a staging environment + * @method static bool isProduction() Determine if the app is in a production environment + * @method static void instance(string $abstract, mixed $concrete) Register an existing instance in the container. + * @method static void bind(string $abstract, \Closure $concrete, bool $shared = false) Register a binding with the container. + * @method static void singleton(string $abstract, \Closure $concrete) Register a shared binding with the container. + * @method static bool has(string $abstract) Determine if the abstract is bound to the container + * @method static mixed get(string $abstract) Resolve an instance + * @method static \Radiate\Foundation\Application getInstance() Get the container instance + * @method static \Radiate\Foundation\Application setInstance(\Radiate\Container\Container $container = null) Set the container instance + * + * @see \Radiate\Foundation\Application, \Radiate\Container\Container + */ class App extends Facade { /** diff --git a/Radiate/Support/Facades/Arr.php b/Radiate/Support/Facades/Arr.php new file mode 100644 index 0000000..37537b4 --- /dev/null +++ b/Radiate/Support/Facades/Arr.php @@ -0,0 +1,83 @@ +$method(...$parameters); + + return $arr instanceof Collection ? $arr->toArray() : $arr; + } +} diff --git a/Radiate/Support/Facades/Event.php b/Radiate/Support/Facades/Event.php index 6cb6263..1ada4e6 100644 --- a/Radiate/Support/Facades/Event.php +++ b/Radiate/Support/Facades/Event.php @@ -2,6 +2,16 @@ namespace Radiate\Support\Facades; +/** + * @method static void listen(string|string[] $events, mixed $listener, int $priority = 10, int $argCount = 5) Create an event listener + * @method static bool hasListeners(string $event) Determine if an event has listeners + * @method static mixed dispatch(string $event, mixed ...$payload) Dispatch an event + * @method static void forget(string $event) Forget the event listeners + * @method static void subscribe(string $subscriber) Register an event subscriber with the dispatcher. + * @method static mixed resolveListener(mixed $listener) Resolve a listener + * + * @see \Radiate\Events\Dispatcher + */ class Event extends Facade { /** diff --git a/Radiate/Support/Facades/Route.php b/Radiate/Support/Facades/Route.php index 563b0cd..ef9edcc 100644 --- a/Radiate/Support/Facades/Route.php +++ b/Radiate/Support/Facades/Route.php @@ -2,6 +2,32 @@ namespace Radiate\Support\Facades; +/** + * @method static \Radiate\Routing\Route ajax(string $uri, mixed $action) Create an ajax route + * @method static \Radiate\Routing\Route get(string $uri, mixed $action) Create a GET route + * @method static \Radiate\Routing\Route post(string $uri, mixed $action) Create a POST route + * @method static \Radiate\Routing\Route put(string $uri, mixed $action) Create a PUT route + * @method static \Radiate\Routing\Route patch(string $uri, mixed $action) Create a PATCH route + * @method static \Radiate\Routing\Route delete(string $uri, mixed $action) Create a DELETE route + * @method static \Radiate\Routing\Route any(string $uri, mixed $action) Create a route with any method + * @method static \Radiate\Routing\Route matches(array $methods, string $uri, mixed $action) Create a route matching the given methods + * @method static void resource(string $uri, string $action, array $methods = ['index', 'show', 'store', 'update', 'destroy']) Create resource routes + * @method static \Radiate\Routing\Route addRoute(\Radiate\Routing\Route $route) Regsiter the route in the router + * @method static bool hasGroupStack() Determine if the router currently has a group stack. + * @method static array getMergedGroupStack() Return the merged group stack + * @method static \Radiate\Routing\Router middleware(string|array $middleware) Set a group middleware + * @method static array getMiddleware() Get the group middleware + * @method static \Radiate\Routing\Router prefix(string $prefix) Set the group prefix + * @method static string getPrefix() Get the group prefix + * @method static \Radiate\Routing\Router namespace(string $namespace) Set the group namespace + * @method static string getNamespace() Get the group namespace + * @method static void group(\Closure|string $routes) Define a group in the router + * @method static void dispatch(\Radiate\Http\Request $request) Dispatch the request to the routes + * @method static void listen(string|string[] $events, mixed $callback) Listen to an event + * + * @see \Radiate\Routing\Router + */ + class Route extends Facade { /** diff --git a/Radiate/Support/Facades/Str.php b/Radiate/Support/Facades/Str.php new file mode 100644 index 0000000..3bb5182 --- /dev/null +++ b/Radiate/Support/Facades/Str.php @@ -0,0 +1,97 @@ +$method(...$parameters); + + return $str instanceof Stringable ? $str->toString() : $str; + } +} diff --git a/Radiate/Support/Facades/View.php b/Radiate/Support/Facades/View.php index 0d23d22..dac6478 100644 --- a/Radiate/Support/Facades/View.php +++ b/Radiate/Support/Facades/View.php @@ -2,6 +2,11 @@ namespace Radiate\Support\Facades; +/** + * @method static string make(string $path, array $args = []) Make a view + * + * @see \Radiate\View\View + */ class View extends Facade { /** diff --git a/Radiate/Support/Hash.php b/Radiate/Support/Hash.php new file mode 100644 index 0000000..4ade1b3 --- /dev/null +++ b/Radiate/Support/Hash.php @@ -0,0 +1,32 @@ + '\1tatuses', + '/(quiz)$/i' => '\1zes', + '/^(ox)$/i' => '\1\2en', + '/([m|l])ouse$/i' => '\1ice', + '/(matr|vert)(ix|ex)$/i' => '\1ices', + '/(x|ch|ss|sh)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(hive)$/i' => '\1s', + '/(chef)$/i' => '\1s', + '/(?:([^f])fe|([lre])f)$/i' => '\1\2ves', + '/sis$/i' => 'ses', + '/([ti])um$/i' => '\1a', + '/(p)erson$/i' => '\1eople', + '/(? '\1en', + '/(c)hild$/i' => '\1hildren', + '/(buffal|tomat)o$/i' => '\1\2oes', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin)us$/i' => '\1i', + '/us$/i' => 'uses', + '/(alias)$/i' => '\1es', + '/(ax|cris|test)is$/i' => '\1es', + '/s$/' => 's', + '/^$/' => '', + '/$/' => 's', + ]; + + /** + * Singular inflector rules + * + * @var array + */ + protected static $singular = [ + '/(s)tatuses$/i' => '\1\2tatus', + '/^(.*)(menu)s$/i' => '\1\2', + '/(quiz)zes$/i' => '\\1', + '/(matr)ices$/i' => '\1ix', + '/(vert|ind)ices$/i' => '\1ex', + '/^(ox)en/i' => '\1', + '/(alias)(es)*$/i' => '\1', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us', + '/([ftw]ax)es/i' => '\1', + '/(cris|ax|test)es$/i' => '\1is', + '/(shoe)s$/i' => '\1', + '/(o)es$/i' => '\1', + '/ouses$/' => 'ouse', + '/([^a])uses$/' => '\1us', + '/([m|l])ice$/i' => '\1ouse', + '/(x|ch|ss|sh)es$/i' => '\1', + '/(m)ovies$/i' => '\1\2ovie', + '/(s)eries$/i' => '\1\2eries', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/(tive)s$/i' => '\1', + '/(hive)s$/i' => '\1', + '/(drive)s$/i' => '\1', + '/([le])ves$/i' => '\1f', + '/([^rfoa])ves$/i' => '\1fe', + '/(^analy)ses$/i' => '\1sis', + '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', + '/([ti])a$/i' => '\1um', + '/(p)eople$/i' => '\1\2erson', + '/(m)en$/i' => '\1an', + '/(c)hildren$/i' => '\1\2hild', + '/(n)ews$/i' => '\1\2ews', + '/eaus$/' => 'eau', + '/^(.*us)$/' => '\\1', + '/s$/i' => '', + ]; + + /** + * Irregular rules + * + * @var array + */ + protected static $irregular = [ + 'atlas' => 'atlases', + 'beef' => 'beefs', + 'brief' => 'briefs', + 'brother' => 'brothers', + 'cafe' => 'cafes', + 'child' => 'children', + 'cookie' => 'cookies', + 'corpus' => 'corpuses', + 'cow' => 'cows', + 'criterion' => 'criteria', + 'ganglion' => 'ganglions', + 'genie' => 'genies', + 'genus' => 'genera', + 'graffito' => 'graffiti', + 'hoof' => 'hoofs', + 'loaf' => 'loaves', + 'man' => 'men', + 'money' => 'monies', + 'mongoose' => 'mongooses', + 'move' => 'moves', + 'mythos' => 'mythoi', + 'niche' => 'niches', + 'numen' => 'numina', + 'occiput' => 'occiputs', + 'octopus' => 'octopuses', + 'opus' => 'opuses', + 'ox' => 'oxen', + 'penis' => 'penises', + 'person' => 'people', + 'sex' => 'sexes', + 'soliloquy' => 'soliloquies', + 'testis' => 'testes', + 'trilby' => 'trilbys', + 'turf' => 'turfs', + 'potato' => 'potatoes', + 'hero' => 'heroes', + 'tooth' => 'teeth', + 'goose' => 'geese', + 'foot' => 'feet', + 'foe' => 'foes', + 'sieve' => 'sieves', + 'cache' => 'caches', + ]; + + /** + * Words that should not be inflected + * + * @var array + */ + protected static $uninflected = [ + '.*?media', '.*[nrlm]ese', '.*data', '.*deer', '.*fish', '.*measles', + '.*ois', '.*pox', '.*sheep', 'audio', 'bison', 'cattle', 'chassis', + 'chassis', 'clippers', 'compensation', 'coreopsis', 'debris', 'diabetes', + 'education', 'emoji', 'equipment', 'evidence', 'feedback', 'firmware', + 'furniture', 'gallows', 'gold', 'graffiti', 'hardware', 'headquarters', + 'information', 'innings', 'jedi', 'kin', 'knowledge', 'money', 'moose', + 'news', 'nexus', 'nutrition', 'offspring', 'people', 'plankton', + 'pokemon', 'police', 'proceedings', 'rain', 'recommended', 'related', + 'research', 'rice', 'sea[- ]bass', 'series', 'software', 'species', + 'stadia', 'swine', 'traffic', 'weather', 'wheat', + ]; + + /** + * Method cache array. + * + * @var array + */ + protected static $cache = []; + + /** + * The initial state of Inflector so reset() works. + * + * @var array + */ + protected static $initialState = []; + + /** + * Cache inflected values, and return if already available + * + * @param string $type Inflection type + * @param string $key Original value + * @param string|false $value Inflected value + * @return string|false Inflected value on cache hit or false on cache miss. + */ + protected static function cache(string $type, string $key, $value = false) + { + $key = '_' . $key; + $type = '_' . $type; + if ($value !== false) { + static::$cache[$type][$key] = $value; + + return $value; + } + if (!isset(static::$cache[$type][$key])) { + return false; + } + + return static::$cache[$type][$key]; + } + + /** + * Clears Inflectors inflected value caches. And resets the inflection + * rules to the initial values. + * + * @return void + */ + public static function reset(): void + { + if (empty(static::$initialState)) { + static::$initialState = get_class_vars(self::class); + + return; + } + foreach (static::$initialState as $key => $val) { + if ($key !== 'initialState') { + static::${$key} = $val; + } + } + } + + /** + * Adds custom inflection $rules, of either 'plural', 'singular', + * 'uninflected' or 'irregular' $type. + * + * ### Usage: + * + * ``` + * Inflector::rules('plural', ['/^(inflect)or$/i' => '\1ables']); + * Inflector::rules('irregular', ['red' => 'redlings']); + * Inflector::rules('uninflected', ['dontinflectme']); + * ``` + * + * @param string $type The type of inflection, either 'plural', 'singular', + * or 'uninflected'. + * @param array $rules Array of rules to be added. + * @param bool $reset If true, will unset default inflections for all + * new rules that are being defined in $rules. + * @return void + */ + public static function rules(string $type, array $rules, bool $reset = false): void + { + $var = '_' . $type; + + if ($reset) { + static::${$var} = $rules; + } elseif ($type === 'uninflected') { + static::$uninflected = array_merge( + $rules, + static::$uninflected + ); + } else { + static::${$var} = $rules + static::${$var}; + } + + static::$cache = []; + } + + /** + * Return $word in plural form. + * + * @param string $word Word in singular + * @return string Word in plural + * @link https://book.cakephp.org/4/en/core-libraries/inflector.html#creating-plural-singular-forms + */ + public static function pluralize(string $word): string + { + if (isset(static::$cache['pluralize'][$word])) { + return static::$cache['pluralize'][$word]; + } + + if (!isset(static::$cache['irregular']['pluralize'])) { + $words = array_keys(static::$irregular); + static::$cache['irregular']['pluralize'] = '/(.*?(?:\\b|_))(' . implode('|', $words) . ')$/i'; + + $upperWords = array_map('ucfirst', $words); + static::$cache['irregular']['upperPluralize'] = '/(.*?(?:\\b|[a-z]))(' . implode('|', $upperWords) . ')$/'; + } + + if ( + preg_match(static::$cache['irregular']['pluralize'], $word, $regs) || + preg_match(static::$cache['irregular']['upperPluralize'], $word, $regs) + ) { + static::$cache['pluralize'][$word] = $regs[1] . substr($regs[2], 0, 1) . + substr(static::$irregular[strtolower($regs[2])], 1); + + return static::$cache['pluralize'][$word]; + } + + if (!isset(static::$cache['uninflected'])) { + static::$cache['uninflected'] = '/^(' . implode('|', static::$uninflected) . ')$/i'; + } + + if (preg_match(static::$cache['uninflected'], $word, $regs)) { + static::$cache['pluralize'][$word] = $word; + + return $word; + } + + foreach (static::$plural as $rule => $replacement) { + if (preg_match($rule, $word)) { + static::$cache['pluralize'][$word] = preg_replace($rule, $replacement, $word); + + return static::$cache['pluralize'][$word]; + } + } + + return $word; + } + + /** + * Return $word in singular form. + * + * @param string $word Word in plural + * @return string Word in singular + * @link https://book.cakephp.org/4/en/core-libraries/inflector.html#creating-plural-singular-forms + */ + public static function singularize(string $word): string + { + if (isset(static::$cache['singularize'][$word])) { + return static::$cache['singularize'][$word]; + } + + if (!isset(static::$cache['irregular']['singular'])) { + $wordList = array_values(static::$irregular); + static::$cache['irregular']['singular'] = '/(.*?(?:\\b|_))(' . implode('|', $wordList) . ')$/i'; + + $upperWordList = array_map('ucfirst', $wordList); + static::$cache['irregular']['singularUpper'] = '/(.*?(?:\\b|[a-z]))(' . + implode('|', $upperWordList) . + ')$/'; + } + + if ( + preg_match(static::$cache['irregular']['singular'], $word, $regs) || + preg_match(static::$cache['irregular']['singularUpper'], $word, $regs) + ) { + $suffix = array_search(strtolower($regs[2]), static::$irregular, true); + $suffix = $suffix ? substr($suffix, 1) : ''; + static::$cache['singularize'][$word] = $regs[1] . substr($regs[2], 0, 1) . $suffix; + + return static::$cache['singularize'][$word]; + } + + if (!isset(static::$cache['uninflected'])) { + static::$cache['uninflected'] = '/^(' . implode('|', static::$uninflected) . ')$/i'; + } + + if (preg_match(static::$cache['uninflected'], $word, $regs)) { + static::$cache['pluralize'][$word] = $word; + + return $word; + } + + foreach (static::$singular as $rule => $replacement) { + if (preg_match($rule, $word)) { + static::$cache['singularize'][$word] = preg_replace($rule, $replacement, $word); + + return static::$cache['singularize'][$word]; + } + } + static::$cache['singularize'][$word] = $word; + + return $word; + } +} diff --git a/Radiate/Routing/Pipeline.php b/Radiate/Support/Pipeline.php similarity index 99% rename from Radiate/Routing/Pipeline.php rename to Radiate/Support/Pipeline.php index 7f94f7c..e58ff04 100644 --- a/Radiate/Routing/Pipeline.php +++ b/Radiate/Support/Pipeline.php @@ -1,6 +1,6 @@ string = $string; + } + + /** + * Return a new instance + * + * @param string $string + * @return static + */ + public static function of(string $string) + { + return new static($string); + } + + /** + * Return the remainder of a string after the first occurrence of a given value. + * + * @param string $search + * @return static + */ + public function after(string $search) + { + if ($search === '') { + return $this; + } + + return new static(array_reverse(explode($search, $this->string, 2))[0]); + } + + /** + * Return the remainder of a string after the last occurrence of a given value. + * + * @param string $search + * @return static + */ + public function afterLast(string $search) + { + if ($search === '') { + return $this; + } + + $position = strrpos($this->string, $search); + + if ($position === false) { + return $this; + } + + return new static(substr($this->string, $position + strlen($search))); + } + + /** + * Append the given values to the string. + * + * @param string ...$values + * @return static + */ + public function append(string ...$values) + { + return new static($this->string . implode('', $values)); + } + + /** + * Get the trailing name component of the path. + * + * @param string $suffix + * @return static + */ + public function basename(string $suffix = '') + { + return new static(basename($this->string, $suffix)); + } + + /** + * Get the portion of a string before the first occurrence of a given value. + * + * @param string $search + * @return static + */ + public function before(string $search) + { + if ($search === '') { + return $this; + } + + $result = strstr($this->string, $search, true); + + return new static($result === false ? $this->string : $result); + } + + /** + * Get the portion of a string before the last occurrence of a given value. + * + * @param string $search + * @return static + */ + public function beforeLast(string $search) + { + if ($search === '') { + return $this; + } + + $pos = mb_strrpos($this->string, $search); + + if ($pos === false) { + return $this; + } + + return $this->substr(0, $pos); + } + + /** + * Get the portion of a string between two given values. + * + * @param string $from + * @param string $to + * @return static + */ + public function between(string $from, string $to) + { + if ($from === '' || $to === '') { + return $this; + } + + return $this->after($from)->beforeLast($to); + } + + /** + * Convert a value to camel case. + * + * @return static + */ + public function camel() + { + $key = $this->string; + + if (isset(static::$camelCache[$key])) { + return static::$camelCache[$key]; + } + + static::$camelCache[$key] = lcfirst($this->studly($key)); + + return new static(static::$camelCache[$key]); + } + + /** + * Determine if a given string contains a given substring. + * + * @param string|string[] $needles + * @return bool + */ + public function contains($needles) + { + foreach ((array) $needles as $needle) { + if ($needle !== '' && mb_strpos($this->string, $needle) !== false) { + return true; + } + } + + return false; + } + + /** + * Determine if a given string contains all array values. + * + * @param string[] $needles + * @return bool + */ + public function containsAll(array $needles) + { + foreach ($needles as $needle) { + if (!$this->contains($needle)) { + return false; + } + } + + return true; + } + + /** + * Get the parent directory's path. + * + * @param int $levels + * @return static + */ + public function dirname(int $levels = 1) + { + return new static(dirname($this->string, $levels)); + } + + /** + * Determine if a given string ends with a given substring. + * + * @param string|string[] $needles + * @return bool + */ + public function endsWith($needles) + { + foreach ((array) $needles as $needle) { + if ($needle !== '' && substr($this->string, -strlen($needle)) === (string) $needle) { + return true; + } + } + + return false; + } + + /** + * Determine if the string is an exact match with the given value. + * + * @param string $value + * @return bool + */ + public function exactly(string $value): bool + { + return $this->string === $value; + } + + /** + * Explode the string into an array. + * + * @param string $delimiter + * @param int $limit + * @return array + */ + public function explode(string $delimiter, int $limit = PHP_INT_MAX): array + { + return explode($delimiter, $this->string, $limit); + } + + /** + * Cap a string with a single instance of a given value. + * + * @param string $cap + * @return static + */ + public function finish(string $cap) + { + return new static(rtrim($this->string, $cap) . $cap); + } + + /** + * Determine if a given string matches a given pattern. + * + * @param string|array $pattern + * @return bool + */ + public function is($pattern): bool + { + $patterns = is_array($pattern) ? $pattern : [$pattern]; + + if (empty($patterns)) { + return false; + } + + foreach ($patterns as $pattern) { + if ($pattern === $this->string) { + return true; + } + + $pattern = preg_quote($pattern, '#'); + + $pattern = str_replace('\*', '.*', $pattern); + + if (preg_match('#^' . $pattern . '\z#u', $this->string) === 1) { + return true; + } + } + + return false; + } + + /** + * Determine if the given string is empty. + * + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->string); + } + + /** + * Determine if the given string is not empty. + * + * @return bool + */ + public function isNotEmpty(): bool + { + return !$this->isEmpty(); + } + + /** + * Determine if a given string is a valid UUID. + * + * @return bool + */ + public function isUuid(): bool + { + return preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $this->string) > 0; + } + + /** + * Convert a string to kebab case. + * + * @return static + */ + public function kebab() + { + return $this->snake('-'); + } + + /** + * Return the length of the given string. + * + * @param string|null $encoding + * @return int + */ + public function length(?string $encoding = null): int + { + if ($encoding) { + return mb_strlen($this->string, $encoding); + } + + return mb_strlen($this->string); + } + + /** + * Limit the number of characters in a string. + * + * @param int $limit + * @param string $end + * @return static + */ + public function limit(int $limit = 100, string $end = '...') + { + if (mb_strwidth($this->string, 'UTF-8') <= $limit) { + return $this->string; + } + + return new static(rtrim(mb_strimwidth($this->string, 0, $limit, '', 'UTF-8')) . $end); + } + + /** + * Convert the given string to lower-case. + * + * @return static + */ + public function lower() + { + return new static(mb_strtolower($this->string, 'UTF-8')); + } + + /** + * Converts Markdown into HTML. + * + * @return string + */ + public function markdown() + { + return (new Parsedown)->text($this->string); + } + + /** + * Get the string matching the given pattern. + * + * @param string $pattern + * @return static|null + */ + public function match(string $pattern) + { + preg_match($pattern, $this->string, $matches); + + if (!$matches) { + return new static; + } + + return new static($matches[1] ?? $matches[0]); + } + + /** + * Get the string matching the given pattern. + * + * @param string $pattern + * @return array + */ + public function matchAll(string $pattern): array + { + preg_match_all($pattern, $this->string, $matches); + + if (empty($matches[0])) { + return []; + } + + return $matches[1] ?? $matches[0]; + } + + /** + * Pad both sides of a string with another. + * + * @param int $length + * @param string $pad + * @return static + */ + public function padBoth(int $length, string $pad = ' ') + { + return new static(str_pad($this->string, $length, $pad, STR_PAD_BOTH)); + } + + /** + * Pad the left side of a string with another. + * + * @param int $length + * @param string $pad + * @return static + */ + public function padLeft(int $length, string $pad = ' ') + { + return new static(str_pad($this->string, $length, $pad, STR_PAD_LEFT)); + } + + /** + * Pad the right side of a string with another. + * + * @param int $length + * @param string $pad + * @return static + */ + public function padRight(int $length, string $pad = ' ') + { + return new static(str_pad($this->string, $length, $pad, STR_PAD_RIGHT)); + } + + /** + * Call the given callback and return a new string. + * + * @param callable $callback + * @return static + */ + public function pipe(callable $callback) + { + return new static(call_user_func($callback, $this)); + } + + /** + * Get the plural form of an English word. + * + * @param int $count + * @return static + */ + public function plural(int $count = 2) + { + return new static(Pluralizer::plural($this->string, $count)); + } + + /** + * Pluralize the last word of an English, studly caps case string. + * + * @param int $count + * @return static + */ + public function pluralStudly(int $count = 2) + { + $parts = preg_split('/(.)(?=[A-Z])/u', $this->string, -1, PREG_SPLIT_DELIM_CAPTURE); + $lastWord = new static(array_pop($parts)); + + return new static(implode('', $parts) . $lastWord->plural($count)); + } + + /** + * Prepend the given values to the string. + * + * @param array ...$values + * @return static + */ + public function prepend(string ...$values) + { + return new static(implode('', $values) . $this->string); + } + + /** + * Generate a more truly "random" alpha-numeric string. + * + * @param int $length + * @return static + */ + public function random(int $length = 16) + { + $string = ''; + + while (($len = strlen($string)) < $length) { + $size = $length - $len; + + $bytes = random_bytes($size); + + $string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size); + } + + return new static($string); + } + + /** + * Replace the given value in the given string. + * + * @param string|string[] $search + * @param string|string[] $replace + * @return static + */ + public function replace($search, $replace) + { + return new static(str_replace($search, $replace, $this->string)); + } + + /** + * Replace a given value in the string sequentially with an array. + * + * @param string $search + * @param array $replace + * @return static + */ + public function replaceArray(string $search, array $replace) + { + $segments = explode($search, $this->string); + + $result = array_shift($segments); + + foreach ($segments as $segment) { + $result .= (array_shift($replace) ?? $search) . $segment; + } + + return new static($result); + } + + /** + * Replace the first occurrence of a given value in the string. + * + * @param string $search + * @param string $replace + * @return static + */ + public function replaceFirst(string $search, string $replace) + { + if ($search === '') { + return $this; + } + + $position = strpos($this->string, $search); + + if ($position !== false) { + return new static(substr_replace($this->string, $replace, $position, strlen($search))); + } + + return $this; + } + + /** + * Replace the last occurrence of a given value in the string. + * + * @param string $search + * @param string $replace + * @return static + */ + public function replaceLast(string $search, string $replace) + { + if ($search === '') { + return $this; + } + + $position = strrpos($this->string, $search); + + if ($position !== false) { + return new static(substr_replace($this->string, $replace, $position, strlen($search))); + } + + return $this; + } + + /** + * Replace the patterns matching the given regular expression. + * + * @param string $pattern + * @param \Closure|string $replace + * @param int $limit + * @return static + */ + public function replaceMatches(string $pattern, $replace, int $limit = -1) + { + if ($replace instanceof Closure) { + return new static(preg_replace_callback($pattern, $replace, $this->string, $limit)); + } + + return new static(preg_replace($pattern, $replace, $this->string, $limit)); + } + + /** + * Split a string using a regular expression or by length. + * + * @param string|int $pattern + * @param int $limit + * @param int $flags + * @return array + */ + public function split($pattern, int $limit = -1, int $flags = 0): array + { + if (filter_var($pattern, FILTER_VALIDATE_INT) !== false) { + return mb_str_split($this->string, $pattern); + } + + $segments = preg_split($pattern, $this->string, $limit, $flags); + + return !empty($segments) ? $segments : []; + } + + /** + * Begin a string with a single instance of a given value. + * + * @param string $prefix + * @return static + */ + public function start(string $prefix) + { + return new static($prefix . ltrim($this->string, $prefix)); + } + + /** + * Convert the given string to upper-case. + * + * @return static + */ + public function upper() + { + return new static(mb_strtoupper($this->string, 'UTF-8')); + } + + /** + * Convert the given string to title case. + * + * @return static + */ + public function title() + { + return new static(mb_convert_case($this->string, MB_CASE_TITLE, 'UTF-8')); + } + + /** + * Get the singular form of an English word. + * + * @return static + */ + public function singular() + { + return new static(Pluralizer::singular($this->string)); + } + + /** + * Generate a URL friendly "slug" from a given string. + * + * @return static + */ + public function slug() + { + return new static(sanitize_title_with_dashes(remove_accents($this->string))); + } + + /** + * Convert a string to snake case. + * + * @param string $delimiter + * @return static + */ + public function snake(string $delimiter = '_') + { + $key = $this->string; + + if (isset(static::$snakeCache[$key][$delimiter])) { + return static::$snakeCache[$key][$delimiter]; + } + + if (!ctype_lower($key)) { + $value = preg_replace('/\s+/u', '', ucwords($key)); + + $value = mb_strtolower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value)); + } + + static::$snakeCache[$key][$delimiter] = $value; + + return new static(static::$snakeCache[$key][$delimiter]); + } + + /** + * Determine if a given string starts with a given substring. + * + * @param string|string[] $needles + * @return bool + */ + public function startsWith($needles) + { + foreach ((array) $needles as $needle) { + if ((string) $needle !== '' && strncmp($this->string, $needle, strlen($needle)) === 0) { + return true; + } + } + + return false; + } + + /** + * Convert a value to studly caps case. + * + * @return static + */ + public function studly() + { + $key = $this->string; + + if (isset(static::$studlyCache[$key])) { + return static::$studlyCache[$key]; + } + + $value = ucwords(str_replace(['-', '_'], ' ', $key)); + + static::$studlyCache[$key] = str_replace(' ', '', $value); + + return new static(static::$studlyCache[$key]); + } + + /** + * Returns the portion of string specified by the start and length parameters. + * + * @param int $start + * @param int|null $length + * @return static + */ + public function substr(int $start, ?int $length = null) + { + return new static(mb_substr($this->string, $start, $length, 'UTF-8')); + } + + /** + * Returns the number of substring occurrences. + * + * @param string $needle + * @param int $offset + * @param int|null $length + * @return int + */ + public function substrCount(string $needle, int $offset = 0, ?int $length = null): int + { + if (!is_null($length)) { + return substr_count($this->string, $needle, $offset, $length); + } else { + return substr_count($this->string, $needle, $offset); + } + } + + /** + * Trim the string of the given characters. + * + * @param string $characters + * @return static + */ + public function trim(?string $characters = " \t\n\r\0\x0B") + { + return new static(trim($this->string, $characters)); + } + + /** + * Left trim the string of the given characters. + * + * @param string $characters + * @return static + */ + public function ltrim(?string $characters = " \t\n\r\0\x0B") + { + return new static(ltrim($this->string, $characters)); + } + + /** + * Right trim the string of the given characters. + * + * @param string $characters + * @return static + */ + public function rtrim(?string $characters = " \t\n\r\0\x0B") + { + return new static(rtrim($this->string, $characters)); + } + + /** + * Call the given Closure with this instance then return the instance. + * + * @param callable $callback + * @return mixed + */ + public function tap(callable $callback) + { + $callback($this); + + return $this; + } + + /** + * Make a string's first character uppercase. + * + * @return static + */ + public function ucfirst() + { + return new static(ucfirst($this->string)); + } + + /** + * Execute the given callback if the string is empty. + * + * @param callable $callback + * @return static + */ + public function whenEmpty(callable $callback) + { + if ($this->isEmpty()) { + $result = $callback($this); + + return is_null($result) ? $this : $result; + } + + return $this; + } + + /** + * Limit the number of words in a string. + * + * @param int $words + * @param string $end + * @return static + */ + public function words(int $words = 100, string $end = '...') + { + preg_match('/^\s*+(?:\S++\s*+){1,' . $words . '}/u', $this->string, $matches); + + if (!isset($matches[0]) || mb_strlen($this->string) === mb_strlen($matches[0])) { + return $this; + } + + return new static(rtrim($matches[0]) . $end); + } + + /** + * Dump the string. + * + * @return static + */ + public function dump() + { + var_dump($this->string); + + return $this; + } + + /** + * Dump the string and end the script. + * + * @return void + */ + public function dd() + { + $this->dump(); + + die(1); + } + + /** + * Proxy dynamic properties onto methods. + * + * @param string $key + * @return mixed + */ + public function __get(string $key) + { + return $this->{$key}(); + } + + /** + * Get the underlying string + * + * @return string + */ + public function toString(): string + { + return $this->string; + } + + /** + * Get the underlying string + * + * @return string + */ + public function __toString(): string + { + return $this->string; + } +}