From 9703aacedcef55924a9729a02580e8adec998a26 Mon Sep 17 00:00:00 2001 From: isszz Date: Sun, 16 Jul 2023 04:12:37 +0800 Subject: [PATCH] fix and update --- README.md | 39 +- composer.json | 9 +- src/Blade.php | 316 ++++++++ src/{BladeService.php => BladeInstance.php} | 101 ++- src/Contracts/Filesystem/Cloud.php | 14 - src/Contracts/Filesystem/Factory.php | 14 - .../Filesystem/FileExistsException.php | 10 - .../Filesystem/FileNotFoundException.php | 10 - src/Contracts/Filesystem/Filesystem.php | 198 ----- src/Contracts/Support/Arrayable.php | 1 + .../Support/CanBeEscapedWhenCastToString.php | 1 + src/Contracts/Support/Htmlable.php | 1 + src/Contracts/Support/Jsonable.php | 1 + src/Contracts/Support/Renderable.php | 1 + src/Filesystem/Filesystem.php | 673 --------------- src/Support/Arr.php | 277 +++++-- src/Support/Collection.php | 767 +++++++++++++----- src/Support/Enumerable.php | 630 ++++++++++---- src/Support/Facades/Blade.php | 22 - src/Support/HigherOrderCollectionProxy.php | 1 + src/Support/HigherOrderTapProxy.php | 1 + src/Support/HigherOrderWhenProxy.php | 1 + src/Support/HtmlString.php | 1 + src/Support/Js.php | 145 ---- src/Support/Pluralizer.php | 121 --- src/Support/Str.php | 420 +--------- src/Support/Stringable.php | 24 +- src/Support/Traits/Conditionable.php | 1 + src/Support/Traits/EnumeratesValues.php | 548 +++++++++---- src/Support/Traits/Macroable.php | 26 +- src/Support/helpers.php | 45 +- src/View/AnonymousComponent.php | 2 +- src/View/AppendableAttributeValue.php | 1 + src/View/Compilers/BladeCompiler.php | 12 +- src/View/Compilers/Compiler.php | 104 ++- src/View/Compilers/CompilerInterface.php | 1 + src/View/Compilers/ComponentTagCompiler.php | 17 +- .../Concerns/CompilesAuthorizations.php | 1 + .../Compilers/Concerns/CompilesClasses.php | 1 + .../Compilers/Concerns/CompilesComments.php | 1 + .../Compilers/Concerns/CompilesComponents.php | 1 + .../Concerns/CompilesConditionals.php | 1 + src/View/Compilers/Concerns/CompilesEchos.php | 124 ++- .../Compilers/Concerns/CompilesErrors.php | 34 - .../Compilers/Concerns/CompilesFragments.php | 1 + .../Compilers/Concerns/CompilesHelpers.php | 1 + .../Compilers/Concerns/CompilesIncludes.php | 1 + .../Compilers/Concerns/CompilesInjections.php | 1 + src/View/Compilers/Concerns/CompilesJs.php | 22 - .../Compilers/Concerns/CompilesLayouts.php | 1 + src/View/Compilers/Concerns/CompilesLoops.php | 1 + .../Compilers/Concerns/CompilesRawPhp.php | 1 + .../Compilers/Concerns/CompilesStacks.php | 7 +- .../Compilers/Concerns/CompilesStyles.php | 1 + src/View/Component.php | 4 +- src/View/ComponentAttributeBag.php | 1 + src/View/ComponentSlot.php | 1 + src/View/Concerns/ManagesComponents.php | 3 +- src/View/Concerns/ManagesFragments.php | 1 + src/View/Concerns/ManagesLayouts.php | 1 + src/View/Concerns/ManagesLoops.php | 1 + src/View/Concerns/ManagesStacks.php | 1 + src/View/DynamicComponent.php | 10 +- src/View/Engines/CompilerEngine.php | 7 +- src/View/Engines/PhpEngine.php | 43 +- src/View/Factory.php | 207 ++--- src/View/FileViewFinder.php | 37 +- src/View/View.php | 23 +- src/View/ViewException.php | 1 + src/View/ViewFinderInterface.php | 71 -- src/View/ViewName.php | 20 +- src/facade/View.php | 1 - src/helper.php | 31 +- 73 files changed, 2462 insertions(+), 2758 deletions(-) create mode 100644 src/Blade.php rename src/{BladeService.php => BladeInstance.php} (71%) delete mode 100644 src/Contracts/Filesystem/Cloud.php delete mode 100644 src/Contracts/Filesystem/Factory.php delete mode 100644 src/Contracts/Filesystem/FileExistsException.php delete mode 100644 src/Contracts/Filesystem/FileNotFoundException.php delete mode 100644 src/Contracts/Filesystem/Filesystem.php delete mode 100644 src/Filesystem/Filesystem.php delete mode 100644 src/Support/Facades/Blade.php delete mode 100644 src/Support/Js.php delete mode 100644 src/Support/Pluralizer.php delete mode 100644 src/View/Compilers/Concerns/CompilesErrors.php delete mode 100644 src/View/Compilers/Concerns/CompilesJs.php delete mode 100644 src/View/ViewFinderInterface.php diff --git a/README.md b/README.md index 3023f6e..c652cd0 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ thinkphp8 blade view engine

Minimum PHP Version + Minimum Thinkphp Version Stable Version Total Downloads License @@ -23,20 +24,24 @@ composer require isszz/think-blade // view.php 模板配置, 多应用时, 每个应用的配置可以不同 return [ - // 视图目录名 - 'dir_name' => 'view', + // 模板引擎类型使用Blade + 'type' => 'Blade', // 模版主题 'theme' => '', + // 缓存路径 + 'compiled' => '', + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法 + 'auto_rule' => 1, + // 视图目录名 + 'view_dir_name' => 'view', // 模板起始路径 - 'base_path' => '', - // 模板文件后缀 - 'suffix' => 'blade.php', + 'view_path' => '', + // 模板后缀 + 'view_suffix' => 'html.php', // 模板文件名分隔符 - 'depr' => DIRECTORY_SEPARATOR, - // 缓存路径 - 'compiled' => '', // 默认留空使用runtime目录 - // 是否开启模板编译缓存, 设为false则每次都会重新编译 - 'cache' => true, + 'view_depr' => DIRECTORY_SEPARATOR, + // 是否开启模板编译缓存,设为false则每次都会重新编译 + 'tpl_cache' => true, ]; ``` @@ -55,9 +60,9 @@ return [ Blade 允许你使用 directive 方法自定义指令。当 Blade 编译器遇到自定义指令时,这会调用该指令包含的表达式提供的回调。 ```php -use think\blade\facade\Blade; +use think\facade\View; -Blade::directive('time2str', function($expression) { +View::directive('time2str', function($expression) { return ""; }); ``` @@ -73,9 +78,9 @@ Blade::directive('time2str', function($expression) { 在定义简单的、自定义条件语句时,编写自定义指令比必须的步骤复杂。在这种情况下,think Blade 提供了 View::if 方法,它允许你使用闭包快速度定义条件指令。例如,定义一个校验当前应用的自定义指令 ```php -use think\blade\facade\Blade; +use think\facade\View; -Blade::if('app', function (...$apps) { +View::if('app', function (...$apps) { $appName = app('http')->getName(); if (count($apps) > 0) { @@ -115,7 +120,7 @@ Blade::if('app', function (...$apps) { ### 中间件 挂载 auth 到 app 案例 ```php -use think\blade\facade\View; +use think\facade\View; /** * 认证 @@ -162,7 +167,7 @@ class Auth ### 有条件地编译 class 样式 该`@class`指令有条件地编译 CSS class 样式。该指令接收一个数组,其中数组的键包含你希望添加的一个或多个样式的类名,而值是一个布尔表达式。如果数组元素有一个数值的键,它将始终包含在呈现的 class 列表中: -``` +```html // 多行php代码 @php $isActive = false; @@ -181,7 +186,7 @@ class Auth ``` ### 同样,@style 指令可用于有条件地将内联 CSS 样式添加到一个 HTML 元素中。 -``` +```html // 单行php代码可以简写如下 @php($isActive = true) diff --git a/composer.json b/composer.json index 15d2b34..ff12bc4 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,6 @@ "ext-json": "*", "ext-fileinfo": "*", "ext-mbstring": "*", - "ramsey/uuid": "^4.7", - "doctrine/inflector": "^2.0", - "symfony/finder": "^4.3.4", "topthink/framework": "8.*" }, "autoload": { @@ -25,6 +22,7 @@ ], "psr-4": { "think\\blade\\": "src", + "think\\view\\driver\\": "src", "Illuminate\\Container\\": "src/Container", "Illuminate\\Contracts\\": "src/Contracts", "Illuminate\\Support\\": "src/Support", @@ -37,11 +35,6 @@ }, "minimum-stability": "dev", "extra": { - "think": { - "services": [ - "think\\blade\\BladeService" - ] - }, "branch-alias": { "dev-master": "dev-dev" } diff --git a/src/Blade.php b/src/Blade.php new file mode 100644 index 0000000..23cda80 --- /dev/null +++ b/src/Blade.php @@ -0,0 +1,316 @@ + '', + // 缓存路径 + 'compiled' => '', + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法 + 'auto_rule' => 1, + // 视图目录名 + 'view_dir_name' => 'view', + // 模板起始路径 + 'view_path' => '', + // 模板后缀 + 'view_suffix' => 'html.php', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + // 是否开启模板编译缓存,设为false则每次都会重新编译 + 'tpl_cache' => true, + ]; + + public function __construct(App $app, array $config = []) + { + $this->app = $app; + $this->config = array_merge($this->config, (array) $config); + + if (empty($this->config['compiled'])) { + $this->config['compiled'] = $app->getRuntimePath() . 'view' . DIRECTORY_SEPARATOR; + } + + // 缓存主题路径 + if (!empty($this->config['theme'])) { + $this->config['compiled'] .= $this->config['theme'] . DIRECTORY_SEPARATOR; + } + + // debug 不缓存 + if ($this->app->isDebug()) { + $this->config['tpl_cache'] = false; + } + + if (empty($this->config['view_path'])) { + $path = $app->getAppPath() .'view'. DS; + } else { + $path = realpath($this->config['view_path']) . DS .'view'. DS; + } + + $this->blade = (new BladeInstance( + $app, + $path, + $this->config['compiled'], + $this->config['tpl_cache'], + ))->getViewFactory(); + + $this->blade->addExtension($this->config['view_suffix'] ?: 'html.php', 'blade'); + } + + /** + * 检测是否存在模板文件 + * + * @param string $template 模板文件或者模板规则 + * @return bool + */ + public function exists(string $template): bool + { + $template = $this->normalize($template); + + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + $template = $this->parseTemplate($template); + } + + return is_file($template); + } + + /** + * 渲染模板文件 + * + * @param string $template 模板文件 + * @param array $data 模板变量 + * @return void + */ + public function fetch(string $template, array $data = []): void + { + $templatePath = ''; + + $template = $this->normalize($template); + + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + $templatePath = $this->parseTemplate($template); + } + + // 模板不存在 抛出异常 + if (!$templatePath || !is_file($templatePath)) { + + $app = $this->app->http->getName(); + $controller = $this->app->request->controller(); + + $errorTemplate = $this->normalize($template, true); + if (strpos($template, '@') === false && strpos($template, '/') === false) { + $errorTemplate = $app .'@'. $controller .'.'. $errorTemplate; + } + + throw new ViewNotFoundException( + 'View not exists: ' . $errorTemplate, + $templatePath, + $this->app->http->getName() .'@'. $this->app->request->controller(), + ); + + throw new ViewNotFoundException('View not exists:' . $this->normalize($template, true), $templatePath); + } + + // 记录视图信息 + $this->app['log'] + ->record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]'); + + echo $this->blade->file($templatePath, $data)->render(); + } + + /** + * 渲染模板内容 + * + * @param string $template 模板内容 + * @param array $data 模板变量 + * @return void + */ + public function display(string $template, array $data = []): void + { + echo $this->fetch($template, $data); + } + + /** + * 自动定位模板文件 + * + * @param string $template 模板文件规则 + * @return string + */ + private function parseTemplate(string $template): string + { + $request = $this->app->request; + + $app = null; + $depr = $this->config['view_depr']; + $view = $this->config['view_dir_name'] ?: 'view'; + + // 获取视图根目录 + if (strpos($template, '@')) { + // 跨应用调用 + [$app, $template] = explode('@', $template); + } + + // 多应用模式 + if(class_exists('\think\app\MultiApp')) { + + $appName = is_null($app) ? $this->app->http->getName() : $app; + + if (is_dir($this->app->getAppPath() . $view)) { + $path = (is_null($app) ? $this->app->getAppPath() : $this->app->getBasePath() . $appName). $depr . $view . $depr; + } else { + $path = $this->app->getRootPath() . $view . $depr . $appName . $depr; + } + } else { + // 单应用模式 + $path = $this->app->getRootPath() . $view . $depr; + } + + // 设置主题路径 + if (!empty($this->config['theme'])) { + $path .= $this->config['theme'] . $depr; + } + + if (0 !== strpos($template, '/')) { + $template = str_replace(['/', ':'], $depr, $template); + $controller = $request->controller(); + if (strpos($controller, '.')) { + $pos = strrpos($controller, '.'); + $controller = substr($controller, 0, $pos) . '.' . Str::snake(substr($controller, $pos + 1)); + } else { + $controller = Str::snake($controller); + } + + if ($controller) { + if ('' == $template) { + // 如果模板文件名为空 按照默认规则定位 + if (2 == $this->config['auto_rule']) { + $template = $request->action(true); + } elseif (3 == $this->config['auto_rule']) { + $template = $request->action(); + } else { + $template = Str::snake($request->action()); + } + + $template = str_replace('.', $depr, $controller) . $depr . $template; + } elseif (false === strpos($template, $depr)) { + $template = str_replace('.', $depr, $controller) . $depr . $template; + } + } + } else { + $template = str_replace(['/', ':'], $depr, substr($template, 1)); + } + + $template = $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); + + if (is_file($template)) { + return $template; + } + + // 未设置主题, 尝试先去default查找 + if(empty($this->config['theme'])) { + $default = str_replace( + $depr .'view'. $depr, + $depr .'view'. $depr .'default'. $depr, + $template + ); + + if (is_file($default)) { + return $default; + } + } + + // 默认主题不存在模版, 降级删除default主题继续查找 + if (strpos($template, $depr .'view'. $depr . 'default' . $depr) !== false) { + $default = str_replace( + $depr .'view'. $depr .'default'. $depr, + $depr .'view'. $depr, + $template + ); + + if (is_file($default)) { + return $default; + } + } + + // 已设置主题, 但是找不到模版, 尝试降级为default主题 + if (strpos($template, $depr .'view'. $depr . $this->config['theme'] . $depr) !== false) { + $default = str_replace( + $depr . $this->config['theme'] . $depr, + $depr .'default'. $depr, + $template + ); + + if (is_file($default)) { + return $default; + } + } + + return ''; + } + + /** + * Normalize the given template. + * + * @param string $name + * @return string + */ + private function normalize($template = '', $isRaw = false) + { + if($isRaw && strpos($template, '/')) { + return str_replace('/', '.', $template); + } + + if (strpos($template, '.')) { + $template = str_replace('.', '/', $template); + } + + return $template; + } + + /** + * 配置模板引擎 + * + * @param array $config 参数 + * @return void + */ + public function config(array $config): void + { + $this->config = array_merge($this->config, $config); + } + + /** + * 获取模板引擎配置 + * + * @param string $name 参数名 + * @return mixed + */ + public function getConfig(string $name) + { + return $this->config[$name]; + } + + public function __debugInfo() + { + return [ + 'config' => $this->config, + 'blade' => $this->blade + ]; + } + + public function __call($method, $params) + { + return call_user_func_array([$this->blade, $method], $params); + } +} diff --git a/src/BladeService.php b/src/BladeInstance.php similarity index 71% rename from src/BladeService.php rename to src/BladeInstance.php index e01a0b8..2daa084 100644 --- a/src/BladeService.php +++ b/src/BladeInstance.php @@ -1,84 +1,64 @@ config = $config->get('view'); - - if (empty($this->config['compiled'])) { - $this->config['compiled'] = $this->app->getRuntimePath(); // . 'view' . DS - } - - $this->compiled = $this->config['compiled']; - $this->isCache = $this->config['cache'] ?? false; - $this->compiledExtension = $this->config['compiled_extension'] ?? 'php';*/ - - $this->compiled = $this->app->getRuntimePath(); - - // if(class_exists('\think\app\MultiApp')) {} - - if (is_null($this->files)) { - $this->files = new Filesystem; - } + $this->app = $app; + $this->path = $path; + $this->cachePath = $cachePath; + $this->isCache = $isCache; $this->registerFactory(); - $this->registerViewFinder(); + // $this->registerViewFinder(); $this->registerBladeCompiler(); $this->registerEngineResolver(); + + return $this; } /** @@ -91,7 +71,8 @@ public function registerFactory() $this->app->bind('blade.view', function () { $factory = $this->createFactory( $this->app->get('view.engine.resolver'), - $this->app->get('view.finder') + null, + // $this->app->get('view.finder') ); // We will also set the container instance on this view environment since the @@ -114,7 +95,7 @@ public function registerFactory() */ protected function createFactory($resolver, $finder) { - return new Factory($resolver, $finder, $this->app); + return new Factory($this->app, $resolver, $finder); } /** @@ -125,8 +106,8 @@ protected function createFactory($resolver, $finder) public function registerViewFinder() { $this->app->bind('view.finder', function () { - return new FileViewFinder($this->files, [ - $this->compiled, + return new FileViewFinder([ + $this->path, ]); }); } @@ -140,9 +121,8 @@ public function registerBladeCompiler() { $this->app->bind('blade.compiler', function () { return tap(new BladeCompiler( - $this->files, - // $this->compiled, - // $this->isCache, + $this->cachePath, + $this->isCache, // $this->compiledExtension, ), function ($blade) { $blade->component('dynamic-component', \Illuminate\View\DynamicComponent::class); @@ -180,7 +160,7 @@ public function registerEngineResolver() public function registerFileEngine($resolver) { $resolver->register('file', function () { - return new FileEngine($this->files); + return new FileEngine(); }); } @@ -193,7 +173,7 @@ public function registerFileEngine($resolver) public function registerPhpEngine($resolver) { $resolver->register('php', function () { - return new PhpEngine($this->files); + return new PhpEngine(); }); } @@ -208,11 +188,26 @@ public function registerBladeEngine($resolver) $resolver->register('blade', function () { $compiler = new CompilerEngine($this->app->get('blade.compiler')); - Container::getInstance()->resolving(function() use ($compiler) { + $this->app->resolving(function() use ($compiler) { $compiler->forgetCompiledOrNotExpired(); }); return $compiler; }); } + + /** + * Get the laravel view factory. + * + * @return Factory + */ + public function getViewFactory(): Factory + { + return $this->app->get('blade.view'); + } + + public function __call($method, $params) + { + return call_user_func_array([$this->app->get('blade.compiler'), $method], $params); + } } diff --git a/src/Contracts/Filesystem/Cloud.php b/src/Contracts/Filesystem/Cloud.php deleted file mode 100644 index 86bea26..0000000 --- a/src/Contracts/Filesystem/Cloud.php +++ /dev/null @@ -1,14 +0,0 @@ -exists($path); - } - - /** - * Get the contents of a file. - * - * @param string $path - * @param bool $lock - * @return string - * - * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException - */ - public function get($path, $lock = false) - { - if ($this->isFile($path)) { - return $lock ? $this->sharedGet($path) : file_get_contents($path); - } - - throw new FileNotFoundException("File does not exist at path {$path}"); - } - - /** - * Get contents of a file with shared access. - * - * @param string $path - * @return string - */ - public function sharedGet($path) - { - $contents = ''; - - $handle = fopen($path, 'rb'); - - if ($handle) { - try { - if (flock($handle, LOCK_SH)) { - clearstatcache(true, $path); - - $contents = fread($handle, $this->size($path) ?: 1); - - flock($handle, LOCK_UN); - } - } finally { - fclose($handle); - } - } - - return $contents; - } - - /** - * Get the returned value of a file. - * - * @param string $path - * @param array $data - * @return mixed - * - * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException - */ - public function getRequire($path, array $data = []) - { - if ($this->isFile($path)) { - $__path = $path; - $__data = $data; - - return (static function () use ($__path, $__data) { - extract($__data, EXTR_SKIP); - - return require $__path; - })(); - } - - throw new FileNotFoundException("File does not exist at path {$path}."); - } - - /** - * Require the given file once. - * - * @param string $path - * @param array $data - * @return mixed - * - * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException - */ - public function requireOnce($path, array $data = []) - { - if ($this->isFile($path)) { - $__path = $path; - $__data = $data; - - return (static function () use ($__path, $__data) { - extract($__data, EXTR_SKIP); - - return require_once $__path; - })(); - } - - throw new FileNotFoundException("File does not exist at path {$path}."); - } - - /** - * Get the hash of the file at the given path. - * - * @param string $path - * @param string $algorithm - * @return string - */ - public function hash($path, $algorithm = 'md5') - { - return hash_file($algorithm, $path); - } - - /** - * Write the contents of a file. - * - * @param string $path - * @param string $contents - * @param bool $lock - * @return int|bool - */ - public function put($path, $contents, $lock = false) - { - return file_put_contents($path, $contents, $lock ? LOCK_EX : 0); - } - - /** - * Write the contents of a file, replacing it atomically if it already exists. - * - * @param string $path - * @param string $content - * @return void - */ - public function replace($path, $content) - { - // If the path already exists and is a symlink, get the real path... - clearstatcache(true, $path); - - $path = realpath($path) ?: $path; - - $tempPath = tempnam(dirname($path), basename($path)); - - // Fix permissions of tempPath because `tempnam()` creates it with permissions set to 0600... - chmod($tempPath, 0777 - umask()); - - file_put_contents($tempPath, $content); - - rename($tempPath, $path); - } - - /** - * Replace a given string within a given file. - * - * @param array|string $search - * @param array|string $replace - * @param string $path - * @return void - */ - public function replaceInFile($search, $replace, $path) - { - file_put_contents($path, str_replace($search, $replace, file_get_contents($path))); - } - - /** - * Prepend to a file. - * - * @param string $path - * @param string $data - * @return int - */ - public function prepend($path, $data) - { - if ($this->exists($path)) { - return $this->put($path, $data.$this->get($path)); - } - - return $this->put($path, $data); - } - - /** - * Append to a file. - * - * @param string $path - * @param string $data - * @return int - */ - public function append($path, $data) - { - return file_put_contents($path, $data, FILE_APPEND); - } - - /** - * Get or set UNIX mode of a file or directory. - * - * @param string $path - * @param int|null $mode - * @return mixed - */ - public function chmod($path, $mode = null) - { - if ($mode) { - return chmod($path, $mode); - } - - return substr(sprintf('%o', fileperms($path)), -4); - } - - /** - * Delete the file at a given path. - * - * @param string|array $paths - * @return bool - */ - public function delete($paths) - { - $paths = is_array($paths) ? $paths : func_get_args(); - - $success = true; - - foreach ($paths as $path) { - try { - if (! @unlink($path)) { - $success = false; - } - } catch (ErrorException $e) { - $success = false; - } - } - - return $success; - } - - /** - * Move a file to a new location. - * - * @param string $path - * @param string $target - * @return bool - */ - public function move($path, $target) - { - return rename($path, $target); - } - - /** - * Copy a file to a new location. - * - * @param string $path - * @param string $target - * @return bool - */ - public function copy($path, $target) - { - return copy($path, $target); - } - - /** - * Create a hard link to the target file or directory. - * - * @param string $target - * @param string $link - * @return void - */ - public function link($target, $link) - { - if (! windows_os()) { - return symlink($target, $link); - } - - $mode = $this->isDirectory($target) ? 'J' : 'H'; - - exec("mklink /{$mode} ".escapeshellarg($link).' '.escapeshellarg($target)); - } - - /** - * Extract the file name from a file path. - * - * @param string $path - * @return string - */ - public function name($path) - { - return pathinfo($path, PATHINFO_FILENAME); - } - - /** - * Extract the trailing name component from a file path. - * - * @param string $path - * @return string - */ - public function basename($path) - { - return pathinfo($path, PATHINFO_BASENAME); - } - - /** - * Extract the parent directory from a file path. - * - * @param string $path - * @return string - */ - public function dirname($path) - { - return pathinfo($path, PATHINFO_DIRNAME); - } - - /** - * Extract the file extension from a file path. - * - * @param string $path - * @return string - */ - public function extension($path) - { - return pathinfo($path, PATHINFO_EXTENSION); - } - - /** - * Get the file type of a given file. - * - * @param string $path - * @return string - */ - public function type($path) - { - return filetype($path); - } - - /** - * Get the mime-type of a given file. - * - * @param string $path - * @return string|false - */ - public function mimeType($path) - { - return finfo_file(finfo_open(FILEINFO_MIME_TYPE), $path); - } - - /** - * Get the file size of a given file. - * - * @param string $path - * @return int - */ - public function size($path) - { - return filesize($path); - } - - /** - * Get the file's last modification time. - * - * @param string $path - * @return int - */ - public function lastModified($path) - { - return filemtime($path); - } - - /** - * Determine if the given path is a directory. - * - * @param string $directory - * @return bool - */ - public function isDirectory($directory) - { - return is_dir($directory); - } - - /** - * Determine if the given path is readable. - * - * @param string $path - * @return bool - */ - public function isReadable($path) - { - return is_readable($path); - } - - /** - * Determine if the given path is writable. - * - * @param string $path - * @return bool - */ - public function isWritable($path) - { - return is_writable($path); - } - - /** - * Determine if two files are the same by comparing their hashes. - * - * @param string $firstFile - * @param string $secondFile - * @return bool - */ - public function hasSameHash($firstFile, $secondFile) - { - $hash = @md5_file($firstFile); - - return $hash && $hash === @md5_file($secondFile); - } - - /** - * Determine if the given path is a file. - * - * @param string $file - * @return bool - */ - public function isFile($file) - { - return is_file($file); - } - - /** - * Find path names matching a given pattern. - * - * @param string $pattern - * @param int $flags - * @return array - */ - public function glob($pattern, $flags = 0) - { - return glob($pattern, $flags); - } - - /** - * Get an array of all files in a directory. - * - * @param string $directory - * @param bool $hidden - * @return \Symfony\Component\Finder\SplFileInfo[] - */ - public function files($directory, $hidden = false) - { - return iterator_to_array( - Finder::create()->files()->ignoreDotFiles(! $hidden)->in($directory)->depth(0)->sortByName(), - false - ); - } - - /** - * Get all of the files from the given directory (recursive). - * - * @param string $directory - * @param bool $hidden - * @return \Symfony\Component\Finder\SplFileInfo[] - */ - public function allFiles($directory, $hidden = false) - { - return iterator_to_array( - Finder::create()->files()->ignoreDotFiles(! $hidden)->in($directory)->sortByName(), - false - ); - } - - /** - * Get all of the directories within a given directory. - * - * @param string $directory - * @return array - */ - public function directories($directory) - { - $directories = []; - - foreach (Finder::create()->in($directory)->directories()->depth(0)->sortByName() as $dir) { - $directories[] = $dir->getPathname(); - } - - return $directories; - } - - /** - * Create a directory. - * - * @param string $path - * @param int $mode - * @param bool $recursive - * @param bool $force - * @return bool - */ - public function makeDirectory($path, $mode = 0755, $recursive = false, $force = false) - { - if ($force) { - return @mkdir($path, $mode, $recursive); - } - - return mkdir($path, $mode, $recursive); - } - - /** - * Move a directory. - * - * @param string $from - * @param string $to - * @param bool $overwrite - * @return bool - */ - public function moveDirectory($from, $to, $overwrite = false) - { - if ($overwrite && $this->isDirectory($to) && ! $this->deleteDirectory($to)) { - return false; - } - - return @rename($from, $to) === true; - } - - /** - * Copy a directory from one location to another. - * - * @param string $directory - * @param string $destination - * @param int|null $options - * @return bool - */ - public function copyDirectory($directory, $destination, $options = null) - { - if (! $this->isDirectory($directory)) { - return false; - } - - $options = $options ?: FilesystemIterator::SKIP_DOTS; - - // If the destination directory does not actually exist, we will go ahead and - // create it recursively, which just gets the destination prepared to copy - // the files over. Once we make the directory we'll proceed the copying. - if (! $this->isDirectory($destination)) { - $this->makeDirectory($destination, 0777, true); - } - - $items = new FilesystemIterator($directory, $options); - - foreach ($items as $item) { - // As we spin through items, we will check to see if the current file is actually - // a directory or a file. When it is actually a directory we will need to call - // back into this function recursively to keep copying these nested folders. - $target = $destination.'/'.$item->getBasename(); - - if ($item->isDir()) { - $path = $item->getPathname(); - - if (! $this->copyDirectory($path, $target, $options)) { - return false; - } - } - - // If the current items is just a regular file, we will just copy this to the new - // location and keep looping. If for some reason the copy fails we'll bail out - // and return false, so the developer is aware that the copy process failed. - else { - if (! $this->copy($item->getPathname(), $target)) { - return false; - } - } - } - - return true; - } - - /** - * Recursively delete a directory. - * - * The directory itself may be optionally preserved. - * - * @param string $directory - * @param bool $preserve - * @return bool - */ - public function deleteDirectory($directory, $preserve = false) - { - if (! $this->isDirectory($directory)) { - return false; - } - - $items = new FilesystemIterator($directory); - - foreach ($items as $item) { - // If the item is a directory, we can just recurse into the function and - // delete that sub-directory otherwise we'll just delete the file and - // keep iterating through each file until the directory is cleaned. - if ($item->isDir() && ! $item->isLink()) { - $this->deleteDirectory($item->getPathname()); - } - - // If the item is just a file, we can go ahead and delete it since we're - // just looping through and waxing all of the files in this directory - // and calling directories recursively, so we delete the real path. - else { - $this->delete($item->getPathname()); - } - } - - if (! $preserve) { - @rmdir($directory); - } - - return true; - } - - /** - * Remove all of the directories within a given directory. - * - * @param string $directory - * @return bool - */ - public function deleteDirectories($directory) - { - $allDirectories = $this->directories($directory); - - if (! empty($allDirectories)) { - foreach ($allDirectories as $directoryName) { - $this->deleteDirectory($directoryName); - } - - return true; - } - - return false; - } - - /** - * Empty the specified directory of all files and folders. - * - * @param string $directory - * @return bool - */ - public function cleanDirectory($directory) - { - return $this->deleteDirectory($directory, true); - } -} diff --git a/src/Support/Arr.php b/src/Support/Arr.php index 13905d8..e18ea6e 100644 --- a/src/Support/Arr.php +++ b/src/Support/Arr.php @@ -1,7 +1,9 @@ $value) { + static::set($results, $key, $value); + } + + return $results; + } + /** * Get all of the given array except for a specified array of keys. * * @param array $array - * @param array|string $keys + * @param array|string|int|float $keys * @return array */ public static function except($array, $keys) @@ -144,17 +163,25 @@ public static function except($array, $keys) */ public static function exists($array, $key) { + if ($array instanceof Enumerable) { + return $array->has($key); + } + if ($array instanceof ArrayAccess) { return $array->offsetExists($key); } + if (is_float($key)) { + $key = (string) $key; + } + return array_key_exists($key, $array); } /** * Return the first element in an array passing a given truth test. * - * @param array $array + * @param iterable $array * @param callable|null $callback * @param mixed $default * @return mixed @@ -200,7 +227,7 @@ public static function last($array, callable $callback = null, $default = null) /** * Flatten a multi-dimensional array into a single level. * - * @param array $array + * @param iterable $array * @param int $depth * @return array */ @@ -231,7 +258,7 @@ public static function flatten($array, $depth = INF) * Remove one or many array items from a given array using "dot" notation. * * @param array $array - * @param array|string $keys + * @param array|string|int|float $keys * @return void */ public static function forget(&$array, $keys) @@ -260,7 +287,7 @@ public static function forget(&$array, $keys) while (count($parts) > 1) { $part = array_shift($parts); - if (isset($array[$part]) && is_array($array[$part])) { + if (isset($array[$part]) && static::accessible($array[$part])) { $array = &$array[$part]; } else { continue 2; @@ -275,8 +302,8 @@ public static function forget(&$array, $keys) * Get an item from an array using "dot" notation. * * @param \ArrayAccess|array $array - * @param string|int $key - * @param mixed $default + * @param string|int|null $key + * @param mixed $default * @return mixed */ public static function get($array, $key, $default = null) @@ -293,7 +320,7 @@ public static function get($array, $key, $default = null) return $array[$key]; } - if (strpos($key, '.') === false) { + if (! str_contains($key, '.')) { return $array[$key] ?? value($default); } @@ -342,6 +369,38 @@ public static function has($array, $keys) return true; } + /** + * Determine if any of the keys exist in an array using "dot" notation. + * + * @param \ArrayAccess|array $array + * @param string|array $keys + * @return bool + */ + public static function hasAny($array, $keys) + { + if (is_null($keys)) { + return false; + } + + $keys = (array) $keys; + + if (! $array) { + return false; + } + + if ($keys === []) { + return false; + } + + foreach ($keys as $key) { + if (static::has($array, $key)) { + return true; + } + } + + return false; + } + /** * Determines if an array is associative. * @@ -357,6 +416,72 @@ public static function isAssoc(array $array) return array_keys($keys) !== $keys; } + /** + * Determines if an array is a list. + * + * An array is a "list" if all array keys are sequential integers starting from 0 with no gaps in between. + * + * @param array $array + * @return bool + */ + public static function isList($array) + { + return ! self::isAssoc($array); + } + + /** + * Join all items using a string. The final items can use a separate glue string. + * + * @param array $array + * @param string $glue + * @param string $finalGlue + * @return string + */ + public static function join($array, $glue, $finalGlue = '') + { + if ($finalGlue === '') { + return implode($glue, $array); + } + + if (count($array) === 0) { + return ''; + } + + if (count($array) === 1) { + return end($array); + } + + $finalItem = array_pop($array); + + return implode($glue, $array).$finalGlue.$finalItem; + } + + /** + * Key an associative array by a field or using a callback. + * + * @param array $array + * @param callable|array|string $keyBy + * @return array + */ + public static function keyBy($array, $keyBy) + { + return Collection::make($array)->keyBy($keyBy)->all(); + } + + /** + * Prepend the key names of an associative array. + * + * @param array $array + * @param string $prependWith + * @return array + */ + public static function prependKeysWith($array, $prependWith) + { + return Collection::make($array)->mapWithKeys(function ($item, $key) use ($prependWith) { + return [$prependWith.$key => $item]; + })->all(); + } + /** * Get a subset of the items from the given array. * @@ -372,8 +497,8 @@ public static function only($array, $keys) /** * Pluck an array of values from an array. * - * @param array $array - * @param string|array $value + * @param iterable $array + * @param string|array|int|null $value * @param string|array|null $key * @return array */ @@ -421,6 +546,26 @@ protected static function explodePluckParameters($value, $key) return [$value, $key]; } + /** + * Run a map over each of the items in the array. + * + * @param array $array + * @param callable $callback + * @return array + */ + public static function map(array $array, callable $callback) + { + $keys = array_keys($array); + + try { + $items = array_map($callback, $array, $keys); + } catch (ArgumentCountError) { + $items = array_map($callback, $array); + } + + return array_combine($keys, $items); + } + /** * Push an item onto the beginning of an array. * @@ -431,7 +576,7 @@ protected static function explodePluckParameters($value, $key) */ public static function prepend($array, $value, $key = null) { - if (is_null($key)) { + if (func_num_args() == 2) { array_unshift($array, $value); } else { $array = [$key => $value] + $array; @@ -443,9 +588,9 @@ public static function prepend($array, $value, $key = null) /** * Get a value from the array, and remove it. * - * @param array $array - * @param string $key - * @param mixed $default + * @param array $array + * @param string|int $key + * @param mixed $default * @return mixed */ public static function pull(&$array, $key, $default = null) @@ -457,16 +602,28 @@ public static function pull(&$array, $key, $default = null) return $value; } + /** + * Convert the array into a query string. + * + * @param array $array + * @return string + */ + public static function query($array) + { + return http_build_query($array, '', '&', PHP_QUERY_RFC3986); + } + /** * Get one or a specified number of random values from an array. * * @param array $array * @param int|null $number + * @param bool|false $preserveKeys * @return mixed * * @throws \InvalidArgumentException */ - public static function random($array, $number = null) + public static function random($array, $number = null, $preserveKeys = false) { $requested = is_null($number) ? 1 : $number; @@ -490,8 +647,14 @@ public static function random($array, $number = null) $results = []; - foreach ((array) $keys as $key) { - $results[] = $array[$key]; + if ($preserveKeys) { + foreach ((array) $keys as $key) { + $results[$key] = $array[$key]; + } + } else { + foreach ((array) $keys as $key) { + $results[] = $array[$key]; + } } return $results; @@ -502,9 +665,9 @@ public static function random($array, $number = null) * * If no key is given to the method, the entire array will be replaced. * - * @param array $array - * @param string $key - * @param mixed $value + * @param array $array + * @param string|int|null $key + * @param mixed $value * @return array */ public static function set(&$array, $key, $value) @@ -515,8 +678,12 @@ public static function set(&$array, $key, $value) $keys = explode('.', $key); - while (count($keys) > 1) { - $key = array_shift($keys); + foreach ($keys as $i => $key) { + if (count($keys) === 1) { + break; + } + + unset($keys[$i]); // If the key doesn't exist at this depth, we will just create an empty array // to hold the next value, allowing us to create the arrays to hold final @@ -557,7 +724,7 @@ public static function shuffle($array, $seed = null) * Sort the array using the given callback or "dot" notation. * * @param array $array - * @param callable|string|null $callback + * @param callable|array|string|null $callback * @return array */ public static function sort($array, $callback = null) @@ -569,36 +736,31 @@ public static function sort($array, $callback = null) * Recursively sort an array by keys and values. * * @param array $array + * @param int $options + * @param bool $descending * @return array */ - public static function sortRecursive($array) + public static function sortRecursive($array, $options = SORT_REGULAR, $descending = false) { foreach ($array as &$value) { if (is_array($value)) { - $value = static::sortRecursive($value); + $value = static::sortRecursive($value, $options, $descending); } } if (static::isAssoc($array)) { - ksort($array); + $descending + ? krsort($array, $options) + : ksort($array, $options); } else { - sort($array); + $descending + ? rsort($array, $options) + : sort($array, $options); } return $array; } - /** - * Convert the array into a query string. - * - * @param array $array - * @return string - */ - public static function query($array) - { - return http_build_query($array, null, '&', PHP_QUERY_RFC3986); - } - /** * Conditionally compile classes from an array into a CSS class list. * @@ -622,29 +784,6 @@ public static function toCssClasses($array) return implode(' ', $classes); } - /** - * Conditionally compile styles from an array into a style list. - * - * @param array $array - * @return string - */ - public static function toCssStyles($array) - { - $styleList = static::wrap($array); - - $styles = []; - - foreach ($styleList as $class => $constraint) { - if (is_numeric($class)) { - $styles[] = Str::finish($constraint, ';'); - } elseif ($constraint) { - $styles[] = Str::finish($class, ';'); - } - } - - return implode(' ', $styles); - } - /** * Filter the array using the given callback. * @@ -665,7 +804,9 @@ public static function where($array, callable $callback) */ public static function whereNotNull($array) { - return static::where($array, fn ($value) => ! is_null($value)); + return static::where($array, function ($value) { + return ! is_null($value); + }); } /** diff --git a/src/Support/Collection.php b/src/Support/Collection.php index f3c6526..467ce39 100644 --- a/src/Support/Collection.php +++ b/src/Support/Collection.php @@ -1,28 +1,41 @@ + * @implements \Illuminate\Support\Enumerable + */ +class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerable { + /** + * @use \Illuminate\Support\Traits\EnumeratesValues + */ use EnumeratesValues, Macroable; /** * The items contained in the collection. * - * @var array + * @var array */ protected $items = []; /** * Create a new collection. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable|null $items * @return void */ public function __construct($items = []) @@ -31,29 +44,21 @@ public function __construct($items = []) } /** - * Create a new collection by invoking the callback a given amount of times. + * Create a collection with the given range. * - * @param int $number - * @param callable $callback - * @return static + * @param int $from + * @param int $to + * @return static */ - public static function times($number, callable $callback = null) + public static function range($from, $to) { - if ($number < 1) { - return new static; - } - - if (is_null($callback)) { - return new static(range(1, $number)); - } - - return (new static(range(1, $number)))->map($callback); + return new static(range($from, $to)); } /** * Get all of the items in the collection. * - * @return array + * @return array */ public function all() { @@ -63,7 +68,7 @@ public function all() /** * Get a lazy collection for the items in this collection. * - * @return \Illuminate\Support\LazyCollection + * @return \Illuminate\Support\LazyCollection */ public function lazy() { @@ -73,8 +78,8 @@ public function lazy() /** * Get the average value of a given key. * - * @param callable|string|null $callback - * @return mixed + * @param (callable(TValue): float|int)|string|null $callback + * @return float|int|null */ public function avg($callback = null) { @@ -94,15 +99,14 @@ public function avg($callback = null) /** * Get the median of a given key. * - * @param string|array|null $key - * @return mixed + * @param string|array|null $key + * @return float|int|null */ public function median($key = null) { $values = (isset($key) ? $this->pluck($key) : $this) - ->filter(function ($item) { - return ! is_null($item); - })->sort()->values(); + ->filter(fn ($item) => ! is_null($item)) + ->sort()->values(); $count = $values->count(); @@ -124,8 +128,8 @@ public function median($key = null) /** * Get the mode of a given key. * - * @param string|array|null $key - * @return array|null + * @param string|array|null $key + * @return array|null */ public function mode($key = null) { @@ -135,25 +139,22 @@ public function mode($key = null) $collection = isset($key) ? $this->pluck($key) : $this; - $counts = new self; + $counts = new static; - $collection->each(function ($value) use ($counts) { - $counts[$value] = isset($counts[$value]) ? $counts[$value] + 1 : 1; - }); + $collection->each(fn ($value) => $counts[$value] = isset($counts[$value]) ? $counts[$value] + 1 : 1); $sorted = $counts->sort(); $highestValue = $sorted->last(); - return $sorted->filter(function ($value) use ($highestValue) { - return $value == $highestValue; - })->sort()->keys()->all(); + return $sorted->filter(fn ($value) => $value == $highestValue) + ->sort()->keys()->all(); } /** * Collapse the collection of items into a single array. * - * @return static + * @return static */ public function collapse() { @@ -163,7 +164,7 @@ public function collapse() /** * Determine if an item exists in the collection. * - * @param mixed $key + * @param (callable(TValue, TKey): bool)|TValue|string $key * @param mixed $operator * @param mixed $value * @return bool @@ -183,11 +184,27 @@ public function contains($key, $operator = null, $value = null) return $this->contains($this->operatorForWhere(...func_get_args())); } + /** + * Determine if an item is not contained in the collection. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function doesntContain($key, $operator = null, $value = null) + { + return ! $this->contains(...func_get_args()); + } + /** * Cross join with the given lists, returning all possible permutations. * - * @param mixed ...$lists - * @return static + * @template TCrossJoinKey + * @template TCrossJoinValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable ...$lists + * @return static> */ public function crossJoin(...$lists) { @@ -199,7 +216,7 @@ public function crossJoin(...$lists) /** * Get the items in the collection that are not present in the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function diff($items) @@ -208,10 +225,10 @@ public function diff($items) } /** - * Get the items in the collection that are not present in the given items. + * Get the items in the collection that are not present in the given items, using the callback. * - * @param mixed $items - * @param callable $callback + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TValue, TValue): int $callback * @return static */ public function diffUsing($items, callable $callback) @@ -222,7 +239,7 @@ public function diffUsing($items, callable $callback) /** * Get the items in the collection whose keys and values are not present in the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function diffAssoc($items) @@ -231,10 +248,10 @@ public function diffAssoc($items) } /** - * Get the items in the collection whose keys and values are not present in the given items. + * Get the items in the collection whose keys and values are not present in the given items, using the callback. * - * @param mixed $items - * @param callable $callback + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TKey, TKey): int $callback * @return static */ public function diffAssocUsing($items, callable $callback) @@ -245,7 +262,7 @@ public function diffAssocUsing($items, callable $callback) /** * Get the items in the collection whose keys are not present in the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function diffKeys($items) @@ -254,10 +271,10 @@ public function diffKeys($items) } /** - * Get the items in the collection whose keys are not present in the given items. + * Get the items in the collection whose keys are not present in the given items, using the callback. * - * @param mixed $items - * @param callable $callback + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TKey, TKey): int $callback * @return static */ public function diffKeysUsing($items, callable $callback) @@ -268,7 +285,7 @@ public function diffKeysUsing($items, callable $callback) /** * Retrieve duplicate items from the collection. * - * @param callable|null $callback + * @param (callable(TValue): bool)|string|null $callback * @param bool $strict * @return static */ @@ -296,7 +313,7 @@ public function duplicates($callback = null, $strict = false) /** * Retrieve duplicate items from the collection using strict comparison. * - * @param callable|null $callback + * @param (callable(TValue): bool)|string|null $callback * @return static */ public function duplicatesStrict($callback = null) @@ -308,7 +325,7 @@ public function duplicatesStrict($callback = null) * Get the comparison function to detect duplicates. * * @param bool $strict - * @return \Closure + * @return callable(TValue, TValue): bool */ protected function duplicateComparator($strict) { @@ -326,7 +343,7 @@ protected function duplicateComparator($strict) /** * Get all items except for those with the specified keys. * - * @param \Illuminate\Support\Collection|mixed $keys + * @param \Illuminate\Support\Enumerable|array $keys * @return static */ public function except($keys) @@ -343,7 +360,7 @@ public function except($keys) /** * Run a filter over each of the items. * - * @param callable|null $callback + * @param (callable(TValue, TKey): bool)|null $callback * @return static */ public function filter(callable $callback = null) @@ -358,9 +375,11 @@ public function filter(callable $callback = null) /** * Get the first item from the collection passing the given truth test. * - * @param callable|null $callback - * @param mixed $default - * @return mixed + * @template TFirstDefault + * + * @param (callable(TValue, TKey): bool)|null $callback + * @param TFirstDefault|(\Closure(): TFirstDefault) $default + * @return TValue|TFirstDefault */ public function first(callable $callback = null, $default = null) { @@ -371,7 +390,7 @@ public function first(callable $callback = null, $default = null) * Get a flattened array of the items in the collection. * * @param int $depth - * @return static + * @return static */ public function flatten($depth = INF) { @@ -381,7 +400,7 @@ public function flatten($depth = INF) /** * Flip the items in the collection. * - * @return static + * @return static */ public function flip() { @@ -391,7 +410,7 @@ public function flip() /** * Remove an item from the collection by key. * - * @param string|array $keys + * @param TKey|array $keys * @return $this */ public function forget($keys) @@ -406,29 +425,49 @@ public function forget($keys) /** * Get an item from the collection by key. * - * @param mixed $key - * @param mixed $default - * @return mixed + * @template TGetDefault + * + * @param TKey $key + * @param TGetDefault|(\Closure(): TGetDefault) $default + * @return TValue|TGetDefault */ public function get($key, $default = null) { - if ($this->offsetExists($key)) { + if (array_key_exists($key, $this->items)) { return $this->items[$key]; } return value($default); } + /** + * Get an item from the collection by key or add it to collection if it does not exist. + * + * @param mixed $key + * @param mixed $value + * @return mixed + */ + public function getOrPut($key, $value) + { + if (array_key_exists($key, $this->items)) { + return $this->items[$key]; + } + + $this->offsetSet($key, $value = value($value)); + + return $value; + } + /** * Group an associative array by a field or using a callback. * - * @param array|callable|string $groupBy + * @param (callable(TValue, TKey): array-key)|array|string $groupBy * @param bool $preserveKeys - * @return static + * @return static> */ public function groupBy($groupBy, $preserveKeys = false) { - if (is_array($groupBy)) { + if (! $this->useAsCallable($groupBy) && is_array($groupBy)) { $nextGroups = $groupBy; $groupBy = array_shift($nextGroups); @@ -446,7 +485,11 @@ public function groupBy($groupBy, $preserveKeys = false) } foreach ($groupKeys as $groupKey) { - $groupKey = is_bool($groupKey) ? (int) $groupKey : $groupKey; + $groupKey = match (true) { + is_bool($groupKey) => (int) $groupKey, + $groupKey instanceof \Stringable => (string) $groupKey, + default => $groupKey, + }; if (! array_key_exists($groupKey, $results)) { $results[$groupKey] = new static; @@ -468,8 +511,8 @@ public function groupBy($groupBy, $preserveKeys = false) /** * Key an associative array by a field or using a callback. * - * @param callable|string $keyBy - * @return static + * @param (callable(TValue, TKey): array-key)|array|string $keyBy + * @return static */ public function keyBy($keyBy) { @@ -493,7 +536,7 @@ public function keyBy($keyBy) /** * Determine if an item exists in the collection by key. * - * @param mixed $key + * @param TKey|array $key * @return bool */ public function has($key) @@ -501,7 +544,7 @@ public function has($key) $keys = is_array($key) ? $key : func_get_args(); foreach ($keys as $value) { - if (! $this->offsetExists($value)) { + if (! array_key_exists($value, $this->items)) { return false; } } @@ -509,28 +552,55 @@ public function has($key) return true; } + /** + * Determine if any of the keys exist in the collection. + * + * @param mixed $key + * @return bool + */ + public function hasAny($key) + { + if ($this->isEmpty()) { + return false; + } + + $keys = is_array($key) ? $key : func_get_args(); + + foreach ($keys as $value) { + if ($this->has($value)) { + return true; + } + } + + return false; + } + /** * Concatenate values of a given key as a string. * - * @param string $value - * @param string $glue + * @param callable|string $value + * @param string|null $glue * @return string */ public function implode($value, $glue = null) { + if ($this->useAsCallable($value)) { + return implode($glue ?? '', $this->map($value)->all()); + } + $first = $this->first(); - if (is_array($first) || is_object($first)) { - return implode($glue, $this->pluck($value)->all()); + if (is_array($first) || (is_object($first) && ! $first instanceof Stringable)) { + return implode($glue ?? '', $this->pluck($value)->all()); } - return implode($value, $this->items); + return implode($value ?? '', $this->items); } /** * Intersect the collection with the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function intersect($items) @@ -541,7 +611,7 @@ public function intersect($items) /** * Intersect the collection with the given items by key. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function intersectByKeys($items) @@ -561,6 +631,16 @@ public function isEmpty() return empty($this->items); } + /** + * Determine if the collection contains a single item. + * + * @return bool + */ + public function containsOneItem() + { + return $this->count() === 1; + } + /** * Join all items from the collection using a string. The final items can use a separate glue string. * @@ -594,7 +674,7 @@ public function join($glue, $finalGlue = '') /** * Get the keys of the collection items. * - * @return static + * @return static */ public function keys() { @@ -604,9 +684,11 @@ public function keys() /** * Get the last item from the collection. * - * @param callable|null $callback - * @param mixed $default - * @return mixed + * @template TLastDefault + * + * @param (callable(TValue, TKey): bool)|null $callback + * @param TLastDefault|(\Closure(): TLastDefault) $default + * @return TValue|TLastDefault */ public function last(callable $callback = null, $default = null) { @@ -616,9 +698,9 @@ public function last(callable $callback = null, $default = null) /** * Get the values of a given key. * - * @param string|array $value + * @param string|int|array $value * @param string|null $key - * @return static + * @return static */ public function pluck($value, $key = null) { @@ -628,16 +710,14 @@ public function pluck($value, $key = null) /** * Run a map over each of the items. * - * @param callable $callback - * @return static + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return static */ public function map(callable $callback) { - $keys = array_keys($this->items); - - $items = array_map($callback, $this->items, $keys); - - return new static(array_combine($keys, $items)); + return new static(Arr::map($this->items, $callback)); } /** @@ -645,8 +725,11 @@ public function map(callable $callback) * * The callback should return an associative array with a single key/value pair. * - * @param callable $callback - * @return static + * @template TMapToDictionaryKey of array-key + * @template TMapToDictionaryValue + * + * @param callable(TValue, TKey): array $callback + * @return static> */ public function mapToDictionary(callable $callback) { @@ -674,8 +757,11 @@ public function mapToDictionary(callable $callback) * * The callback should return an associative array with a single key/value pair. * - * @param callable $callback - * @return static + * @template TMapWithKeysKey of array-key + * @template TMapWithKeysValue + * + * @param callable(TValue, TKey): array $callback + * @return static */ public function mapWithKeys(callable $callback) { @@ -695,7 +781,7 @@ public function mapWithKeys(callable $callback) /** * Merge the collection with the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function merge($items) @@ -706,8 +792,10 @@ public function merge($items) /** * Recursively merge the collection with the given items. * - * @param mixed $items - * @return static + * @template TMergeRecursiveValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static */ public function mergeRecursive($items) { @@ -717,8 +805,10 @@ public function mergeRecursive($items) /** * Create a collection by using this collection for keys and another for its values. * - * @param mixed $values - * @return static + * @template TCombineValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static */ public function combine($values) { @@ -728,7 +818,7 @@ public function combine($values) /** * Union the collection with the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function union($items) @@ -749,8 +839,8 @@ public function nth($step, $offset = 0) $position = 0; - foreach ($this->items as $item) { - if ($position % $step === $offset) { + foreach ($this->slice($offset)->items as $item) { + if ($position % $step === 0) { $new[] = $item; } @@ -763,7 +853,7 @@ public function nth($step, $offset = 0) /** * Get the items with the specified keys. * - * @param mixed $keys + * @param \Illuminate\Support\Enumerable|array|string $keys * @return static */ public function only($keys) @@ -782,38 +872,57 @@ public function only($keys) } /** - * Get and remove the last item from the collection. + * Get and remove the last N items from the collection. * - * @return mixed + * @param int $count + * @return static|TValue|null */ - public function pop() + public function pop($count = 1) { - return array_pop($this->items); + if ($count === 1) { + return array_pop($this->items); + } + + if ($this->isEmpty()) { + return new static; + } + + $results = []; + + $collectionCount = $this->count(); + + foreach (range(1, min($count, $collectionCount)) as $item) { + array_push($results, array_pop($this->items)); + } + + return new static($results); } /** * Push an item onto the beginning of the collection. * - * @param mixed $value - * @param mixed $key + * @param TValue $value + * @param TKey $key * @return $this */ public function prepend($value, $key = null) { - $this->items = Arr::prepend($this->items, $value, $key); + $this->items = Arr::prepend($this->items, ...func_get_args()); return $this; } /** - * Push an item onto the end of the collection. + * Push one or more items onto the end of the collection. * - * @param mixed $value + * @param TValue ...$values * @return $this */ - public function push($value) + public function push(...$values) { - $this->items[] = $value; + foreach ($values as $value) { + $this->items[] = $value; + } return $this; } @@ -821,7 +930,7 @@ public function push($value) /** * Push all of the given items onto the collection. * - * @param iterable $source + * @param iterable $source * @return static */ public function concat($source) @@ -838,9 +947,11 @@ public function concat($source) /** * Get and remove an item from the collection. * - * @param mixed $key - * @param mixed $default - * @return mixed + * @template TPullDefault + * + * @param TKey $key + * @param TPullDefault|(\Closure(): TPullDefault) $default + * @return TValue|TPullDefault */ public function pull($key, $default = null) { @@ -850,8 +961,8 @@ public function pull($key, $default = null) /** * Put an item in the collection by key. * - * @param mixed $key - * @param mixed $value + * @param TKey $key + * @param TValue $value * @return $this */ public function put($key, $value) @@ -864,8 +975,8 @@ public function put($key, $value) /** * Get one or a specified number of items randomly from the collection. * - * @param int|null $number - * @return static|mixed + * @param (callable(self): int)|int|null $number + * @return static|TValue * * @throws \InvalidArgumentException */ @@ -875,46 +986,17 @@ public function random($number = null) return Arr::random($this->items); } - return new static(Arr::random($this->items, $number)); - } - - /** - * 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); - }*/ - - /** - * Reduce the collection to a single value. - * - * @template TReduceInitial - * @template TReduceReturnType - * - * @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback - * @param TReduceInitial $initial - * @return TReduceReturnType - */ - public function reduce(callable $callback, $initial = null) - { - $result = $initial; - - foreach ($this as $key => $value) { - $result = $callback($result, $value, $key); + if (is_callable($number)) { + return new static(Arr::random($this->items, $number($this))); } - return $result; + return new static(Arr::random($this->items, $number)); } /** * Replace the collection items with the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function replace($items) @@ -925,7 +1007,7 @@ public function replace($items) /** * Recursively replace the collection items with the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function replaceRecursive($items) @@ -946,9 +1028,9 @@ public function reverse() /** * Search the collection for a given value and return the corresponding key if successful. * - * @param mixed $value + * @param TValue|(callable(TValue,TKey): bool) $value * @param bool $strict - * @return mixed + * @return TKey|bool */ public function search($value, $strict = false) { @@ -966,19 +1048,36 @@ public function search($value, $strict = false) } /** - * Get and remove the first item from the collection. + * Get and remove the first N items from the collection. * - * @return mixed + * @param int $count + * @return static|TValue|null */ - public function shift() + public function shift($count = 1) { - return array_shift($this->items); + if ($count === 1) { + return array_shift($this->items); + } + + if ($this->isEmpty()) { + return new static; + } + + $results = []; + + $collectionCount = $this->count(); + + foreach (range(1, min($count, $collectionCount)) as $item) { + array_push($results, array_shift($this->items)); + } + + return new static($results); } /** * Shuffle the items in the collection. * - * @param int $seed + * @param int|null $seed * @return static */ public function shuffle($seed = null) @@ -986,6 +1085,22 @@ public function shuffle($seed = null) return new static(Arr::shuffle($this->items, $seed)); } + /** + * Create chunks representing a "sliding window" view of the items in the collection. + * + * @param int $size + * @param int $step + * @return static + */ + public function sliding($size = 2, $step = 1) + { + $chunks = floor(($this->count() - $size) / $step) + 1; + + return static::times($chunks, function ($number) use ($size, $step) { + return $this->slice(($number - 1) * $step, $size); + }); + } + /** * Skip the first {$count} items. * @@ -997,11 +1112,33 @@ public function skip($count) return $this->slice($count); } + /** + * Skip items in the collection until the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function skipUntil($value) + { + return new static($this->lazy()->skipUntil($value)->all()); + } + + /** + * Skip items in the collection while the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function skipWhile($value) + { + return new static($this->lazy()->skipWhile($value)->all()); + } + /** * Slice the underlying collection array. * * @param int $offset - * @param int $length + * @param int|null $length * @return static */ public function slice($offset, $length = null) @@ -1013,7 +1150,7 @@ public function slice($offset, $length = null) * Split a collection into a certain number of groups. * * @param int $numberOfGroups - * @return static + * @return static */ public function split($numberOfGroups) { @@ -1046,11 +1183,81 @@ public function split($numberOfGroups) return $groups; } + /** + * Split a collection into a certain number of groups, and fill the first groups completely. + * + * @param int $numberOfGroups + * @return static + */ + public function splitIn($numberOfGroups) + { + return $this->chunk(ceil($this->count() / $numberOfGroups)); + } + + /** + * Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception. + * + * @param (callable(TValue, TKey): bool)|string $key + * @param mixed $operator + * @param mixed $value + * @return TValue + * + * @throws \Illuminate\Support\ItemNotFoundException + * @throws \Illuminate\Support\MultipleItemsFoundException + */ + public function sole($key = null, $operator = null, $value = null) + { + $filter = func_num_args() > 1 + ? $this->operatorForWhere(...func_get_args()) + : $key; + + $items = $this->unless($filter == null)->filter($filter); + + $count = $items->count(); + + if ($count === 0) { + throw new ItemNotFoundException; + } + + if ($count > 1) { + throw new MultipleItemsFoundException($count); + } + + return $items->first(); + } + + /** + * Get the first item in the collection but throw an exception if no matching items exist. + * + * @param (callable(TValue, TKey): bool)|string $key + * @param mixed $operator + * @param mixed $value + * @return TValue + * + * @throws \Illuminate\Support\ItemNotFoundException + */ + public function firstOrFail($key = null, $operator = null, $value = null) + { + $filter = func_num_args() > 1 + ? $this->operatorForWhere(...func_get_args()) + : $key; + + $placeholder = new stdClass(); + + $item = $this->first($filter, $placeholder); + + if ($item === $placeholder) { + throw new ItemNotFoundException; + } + + return $item; + } + /** * Chunk the collection into chunks of the given size. * * @param int $size - * @return static + * @return static */ public function chunk($size) { @@ -1067,19 +1274,47 @@ public function chunk($size) return new static($chunks); } + /** + * Chunk the collection into chunks with a callback. + * + * @param callable(TValue, TKey, static): bool $callback + * @return static> + */ + public function chunkWhile(callable $callback) + { + return new static( + $this->lazy()->chunkWhile($callback)->mapInto(static::class) + ); + } + /** * Sort through each item with a callback. * - * @param callable|null $callback + * @param (callable(TValue, TValue): int)|null|int $callback * @return static */ - public function sort(callable $callback = null) + public function sort($callback = null) { $items = $this->items; - $callback + $callback && is_callable($callback) ? uasort($items, $callback) - : asort($items); + : asort($items, $callback ?? SORT_REGULAR); + + return new static($items); + } + + /** + * Sort items in descending order. + * + * @param int $options + * @return static + */ + public function sortDesc($options = SORT_REGULAR) + { + $items = $this->items; + + arsort($items, $options); return new static($items); } @@ -1087,20 +1322,24 @@ public function sort(callable $callback = null) /** * Sort the collection using the given callback. * - * @param callable|string $callback + * @param array|(callable(TValue, TKey): mixed)|string $callback * @param int $options * @param bool $descending * @return static */ public function sortBy($callback, $options = SORT_REGULAR, $descending = false) { + if (is_array($callback) && ! is_callable($callback)) { + return $this->sortByMany($callback); + } + $results = []; $callback = $this->valueRetriever($callback); // First we will loop through the items and get the comparator from a callback // function which we were given. Then, we will sort the returned values and - // and grab the corresponding values for the sorted keys from this array. + // grab all the corresponding values for the sorted keys from this array. foreach ($this->items as $key => $value) { $results[$key] = $callback($value, $key); } @@ -1118,10 +1357,52 @@ public function sortBy($callback, $options = SORT_REGULAR, $descending = false) return new static($results); } + /** + * Sort the collection using multiple comparisons. + * + * @param array $comparisons + * @return static + */ + protected function sortByMany(array $comparisons = []) + { + $items = $this->items; + + uasort($items, function ($a, $b) use ($comparisons) { + foreach ($comparisons as $comparison) { + $comparison = Arr::wrap($comparison); + + $prop = $comparison[0]; + + $ascending = Arr::get($comparison, 1, true) === true || + Arr::get($comparison, 1, true) === 'asc'; + + if (! is_string($prop) && is_callable($prop)) { + $result = $prop($a, $b); + } else { + $values = [data_get($a, $prop), data_get($b, $prop)]; + + if (! $ascending) { + $values = array_reverse($values); + } + + $result = $values[0] <=> $values[1]; + } + + if ($result === 0) { + continue; + } + + return $result; + } + }); + + return new static($items); + } + /** * Sort the collection in descending order using the given callback. * - * @param callable|string $callback + * @param array|(callable(TValue, TKey): mixed)|string $callback * @param int $options * @return static */ @@ -1149,7 +1430,7 @@ public function sortKeys($options = SORT_REGULAR, $descending = false) /** * Sort the collection keys in descending order. * - * @param int $options + * @param int $options * @return static */ public function sortKeysDesc($options = SORT_REGULAR) @@ -1157,12 +1438,27 @@ public function sortKeysDesc($options = SORT_REGULAR) return $this->sortKeys($options, true); } + /** + * Sort the collection keys using a callback. + * + * @param callable(TKey, TKey): int $callback + * @return static + */ + public function sortKeysUsing(callable $callback) + { + $items = $this->items; + + uksort($items, $callback); + + return new static($items); + } + /** * Splice a portion of the underlying collection array. * * @param int $offset * @param int|null $length - * @param mixed $replacement + * @param array $replacement * @return static */ public function splice($offset, $length = null, $replacement = []) @@ -1171,7 +1467,7 @@ public function splice($offset, $length = null, $replacement = []) return new static(array_splice($this->items, $offset)); } - return new static(array_splice($this->items, $offset, $length, $replacement)); + return new static(array_splice($this->items, $offset, $length, $this->getArrayableItems($replacement))); } /** @@ -1189,10 +1485,32 @@ public function take($limit) return $this->slice(0, $limit); } + /** + * Take items in the collection until the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function takeUntil($value) + { + return new static($this->lazy()->takeUntil($value)->all()); + } + + /** + * Take items in the collection while the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function takeWhile($value) + { + return new static($this->lazy()->takeWhile($value)->all()); + } + /** * Transform each item in the collection using a callback. * - * @param callable $callback + * @param callable(TValue, TKey): TValue $callback * @return $this */ public function transform(callable $callback) @@ -1203,10 +1521,46 @@ public function transform(callable $callback) } /** - * Reset the keys on the underlying array. + * Convert a flatten "dot" notation array into an expanded array. + * + * @return static + */ + public function undot() + { + return new static(Arr::undot($this->all())); + } + + /** + * Return only unique items from the collection array. * + * @param (callable(TValue, TKey): mixed)|string|null $key + * @param bool $strict * @return static */ + public function unique($key = null, $strict = false) + { + if (is_null($key) && $strict === false) { + return new static(array_unique($this->items, SORT_REGULAR)); + } + + $callback = $this->valueRetriever($key); + + $exists = []; + + return $this->reject(function ($item, $key) use ($callback, $strict, &$exists) { + if (in_array($id = $callback($item, $key), $exists, $strict)) { + return true; + } + + $exists[] = $id; + }); + } + + /** + * Reset the keys on the underlying array. + * + * @return static + */ public function values() { return new static(array_values($this->items)); @@ -1218,8 +1572,10 @@ public function values() * e.g. new Collection([1, 2, 3])->zip([4, 5, 6]); * => [[1, 4], [2, 5], [3, 6]] * - * @param mixed ...$items - * @return static + * @template TZipValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable ...$items + * @return static> */ public function zip($items) { @@ -1231,15 +1587,17 @@ public function zip($items) return new static(func_get_args()); }, $this->items], $arrayableItems); - return new static(call_user_func_array('array_map', $params)); + return new static(array_map(...$params)); } /** * Pad collection to the specified length with a value. * + * @template TPadValue + * * @param int $size - * @param mixed $value - * @return static + * @param TPadValue $value + * @return static */ public function pad($size, $value) { @@ -1249,9 +1607,9 @@ public function pad($size, $value) /** * Get an iterator for the items. * - * @return \ArrayIterator + * @return \ArrayIterator */ - public function getIterator(): \ArrayIterator + public function getIterator(): Traversable { return new ArrayIterator($this->items); } @@ -1266,10 +1624,21 @@ public function count(): int return count($this->items); } + /** + * Count the number of items in the collection by a field or using a callback. + * + * @param (callable(TValue, TKey): mixed)|string|null $countBy + * @return static + */ + public function countBy($countBy = null) + { + return new static($this->lazy()->countBy($countBy)->all()); + } + /** * Add an item to the collection. * - * @param mixed $item + * @param TValue $item * @return $this */ public function add($item) @@ -1282,7 +1651,7 @@ public function add($item) /** * Get a base Support collection instance from this collection. * - * @return \Illuminate\Support\Collection + * @return \Illuminate\Support\Collection */ public function toBase() { @@ -1292,19 +1661,19 @@ public function toBase() /** * Determine if an item exists at an offset. * - * @param mixed $key + * @param TKey $key * @return bool */ public function offsetExists($key): bool { - return array_key_exists($key, $this->items); + return isset($this->items[$key]); } /** * Get an item at a given offset. * - * @param mixed $key - * @return mixed + * @param TKey $key + * @return TValue */ public function offsetGet($key): mixed { @@ -1314,8 +1683,8 @@ public function offsetGet($key): mixed /** * Set the item at a given offset. * - * @param mixed $key - * @param mixed $value + * @param TKey|null $key + * @param TValue $value * @return void */ public function offsetSet($key, $value): void @@ -1330,7 +1699,7 @@ public function offsetSet($key, $value): void /** * Unset the item at a given offset. * - * @param string $key + * @param TKey $key * @return void */ public function offsetUnset($key): void diff --git a/src/Support/Enumerable.php b/src/Support/Enumerable.php index 02d6601..7832393 100644 --- a/src/Support/Enumerable.php +++ b/src/Support/Enumerable.php @@ -1,20 +1,33 @@ + * @extends \IteratorAggregate + */ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable { /** * Create a new collection instance if the value isn't one already. * - * @param mixed $items - * @return static + * @template TMakeKey of array-key + * @template TMakeValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|null $items + * @return static */ public static function make($items = []); @@ -22,27 +35,49 @@ public static function make($items = []); * Create a new instance by invoking the callback a given amount of times. * * @param int $number - * @param callable $callback + * @param callable|null $callback * @return static */ public static function times($number, callable $callback = null); /** - * Wrap the given value in a collection if applicable. + * Create a collection with the given range. * - * @param mixed $value + * @param int $from + * @param int $to * @return static */ + public static function range($from, $to); + + /** + * Wrap the given value in a collection if applicable. + * + * @template TWrapKey of array-key + * @template TWrapValue + * + * @param iterable $value + * @return static + */ public static function wrap($value); /** * Get the underlying items from the given collection if applicable. * - * @param array|static $value - * @return array + * @template TUnwrapKey of array-key + * @template TUnwrapValue + * + * @param array|static $value + * @return array */ public static function unwrap($value); + /** + * Create a new instance with no items. + * + * @return static + */ + public static function empty(); + /** * Get all items in the enumerable. * @@ -53,38 +88,38 @@ public function all(); /** * Alias for the "avg" method. * - * @param callable|string|null $callback - * @return mixed + * @param (callable(TValue): float|int)|string|null $callback + * @return float|int|null */ public function average($callback = null); /** * Get the median of a given key. * - * @param string|array|null $key - * @return mixed + * @param string|array|null $key + * @return float|int|null */ public function median($key = null); /** * Get the mode of a given key. * - * @param string|array|null $key - * @return array|null + * @param string|array|null $key + * @return array|null */ public function mode($key = null); /** * Collapse the items into a single enumerable. * - * @return static + * @return static */ public function collapse(); /** * Alias for the "contains" method. * - * @param mixed $key + * @param (callable(TValue, TKey): bool)|TValue|string $key * @param mixed $operator * @param mixed $value * @return bool @@ -94,8 +129,8 @@ public function some($key, $operator = null, $value = null); /** * Determine if an item exists, using strict comparison. * - * @param mixed $key - * @param mixed $value + * @param (callable(TValue): bool)|TValue|array-key $key + * @param TValue|null $value * @return bool */ public function containsStrict($key, $value = null); @@ -103,26 +138,47 @@ public function containsStrict($key, $value = null); /** * Get the average value of a given key. * - * @param callable|string|null $callback - * @return mixed + * @param (callable(TValue): float|int)|string|null $callback + * @return float|int|null */ public function avg($callback = null); /** * Determine if an item exists in the enumerable. * - * @param mixed $key + * @param (callable(TValue, TKey): bool)|TValue|string $key * @param mixed $operator * @param mixed $value * @return bool */ public function contains($key, $operator = null, $value = null); + /** + * Determine if an item is not contained in the collection. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function doesntContain($key, $operator = null, $value = null); + + /** + * Cross join with the given lists, returning all possible permutations. + * + * @template TCrossJoinKey + * @template TCrossJoinValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable ...$lists + * @return static> + */ + public function crossJoin(...$lists); + /** * Dump the collection and end the script. * * @param mixed ...$args - * @return void + * @return never */ public function dd(...$args); @@ -136,7 +192,7 @@ public function dump(); /** * Get the items that are not present in the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function diff($items); @@ -144,8 +200,8 @@ public function diff($items); /** * Get the items that are not present in the given items, using the callback. * - * @param mixed $items - * @param callable $callback + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TValue, TValue): int $callback * @return static */ public function diffUsing($items, callable $callback); @@ -153,16 +209,16 @@ public function diffUsing($items, callable $callback); /** * Get the items whose keys and values are not present in the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function diffAssoc($items); /** - * Get the items whose keys and values are not present in the given items. + * Get the items whose keys and values are not present in the given items, using the callback. * - * @param mixed $items - * @param callable $callback + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TKey, TKey): int $callback * @return static */ public function diffAssocUsing($items, callable $callback); @@ -170,16 +226,16 @@ public function diffAssocUsing($items, callable $callback); /** * Get the items whose keys are not present in the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function diffKeys($items); /** - * Get the items whose keys are not present in the given items. + * Get the items whose keys are not present in the given items, using the callback. * - * @param mixed $items - * @param callable $callback + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @param callable(TKey, TKey): int $callback * @return static */ public function diffKeysUsing($items, callable $callback); @@ -187,7 +243,7 @@ public function diffKeysUsing($items, callable $callback); /** * Retrieve duplicate items. * - * @param callable|null $callback + * @param (callable(TValue): bool)|string|null $callback * @param bool $strict * @return static */ @@ -196,7 +252,7 @@ public function duplicates($callback = null, $strict = false); /** * Retrieve duplicate items using strict comparison. * - * @param callable|null $callback + * @param (callable(TValue): bool)|string|null $callback * @return static */ public function duplicatesStrict($callback = null); @@ -204,7 +260,7 @@ public function duplicatesStrict($callback = null); /** * Execute a callback over each item. * - * @param callable $callback + * @param callable(TValue, TKey): mixed $callback * @return $this */ public function each(callable $callback); @@ -218,9 +274,9 @@ public function each(callable $callback); public function eachSpread(callable $callback); /** - * Determine if all items pass the given test. + * Determine if all items pass the given truth test. * - * @param string|callable $key + * @param (callable(TValue, TKey): bool)|TValue|string $key * @param mixed $operator * @param mixed $value * @return bool @@ -230,7 +286,7 @@ public function every($key, $operator = null, $value = null); /** * Get all items except for those with the specified keys. * - * @param mixed $keys + * @param \Illuminate\Support\Enumerable|array $keys * @return static */ public function except($keys); @@ -238,64 +294,76 @@ public function except($keys); /** * Run a filter over each of the items. * - * @param callable|null $callback + * @param (callable(TValue): bool)|null $callback * @return static */ public function filter(callable $callback = null); /** - * Apply the callback if the value is truthy. + * Apply the callback if the given "value" is (or resolves to) truthy. + * + * @template TWhenReturnType as null * * @param bool $value - * @param callable $callback - * @param callable $default - * @return static|mixed + * @param (callable($this): TWhenReturnType)|null $callback + * @param (callable($this): TWhenReturnType)|null $default + * @return $this|TWhenReturnType */ - public function when($value, callable $callback, callable $default = null); + public function when($value, callable $callback = null, callable $default = null); /** * Apply the callback if the collection is empty. * - * @param callable $callback - * @param callable $default - * @return static|mixed + * @template TWhenEmptyReturnType + * + * @param (callable($this): TWhenEmptyReturnType) $callback + * @param (callable($this): TWhenEmptyReturnType)|null $default + * @return $this|TWhenEmptyReturnType */ public function whenEmpty(callable $callback, callable $default = null); /** * Apply the callback if the collection is not empty. * - * @param callable $callback - * @param callable $default - * @return static|mixed + * @template TWhenNotEmptyReturnType + * + * @param callable($this): TWhenNotEmptyReturnType $callback + * @param (callable($this): TWhenNotEmptyReturnType)|null $default + * @return $this|TWhenNotEmptyReturnType */ public function whenNotEmpty(callable $callback, callable $default = null); /** - * Apply the callback if the value is falsy. + * Apply the callback if the given "value" is (or resolves to) truthy. + * + * @template TUnlessReturnType * * @param bool $value - * @param callable $callback - * @param callable $default - * @return static|mixed + * @param (callable($this): TUnlessReturnType) $callback + * @param (callable($this): TUnlessReturnType)|null $default + * @return $this|TUnlessReturnType */ public function unless($value, callable $callback, callable $default = null); /** * Apply the callback unless the collection is empty. * - * @param callable $callback - * @param callable $default - * @return static|mixed + * @template TUnlessEmptyReturnType + * + * @param callable($this): TUnlessEmptyReturnType $callback + * @param (callable($this): TUnlessEmptyReturnType)|null $default + * @return $this|TUnlessEmptyReturnType */ public function unlessEmpty(callable $callback, callable $default = null); /** * Apply the callback unless the collection is not empty. * - * @param callable $callback - * @param callable $default - * @return static|mixed + * @template TUnlessNotEmptyReturnType + * + * @param callable($this): TUnlessNotEmptyReturnType $callback + * @param (callable($this): TUnlessNotEmptyReturnType)|null $default + * @return $this|TUnlessNotEmptyReturnType */ public function unlessNotEmpty(callable $callback, callable $default = null); @@ -309,6 +377,22 @@ public function unlessNotEmpty(callable $callback, callable $default = null); */ public function where($key, $operator = null, $value = null); + /** + * Filter items where the value for the given key is null. + * + * @param string|null $key + * @return static + */ + public function whereNull($key = null); + + /** + * Filter items where the value for the given key is not null. + * + * @param string|null $key + * @return static + */ + public function whereNotNull($key = null); + /** * Filter items by the given key value pair using strict comparison. * @@ -322,7 +406,7 @@ public function whereStrict($key, $value); * Filter items by the given key value pair. * * @param string $key - * @param mixed $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @param bool $strict * @return static */ @@ -332,7 +416,7 @@ public function whereIn($key, $values, $strict = false); * Filter items by the given key value pair using strict comparison. * * @param string $key - * @param mixed $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @return static */ public function whereInStrict($key, $values); @@ -341,7 +425,7 @@ public function whereInStrict($key, $values); * Filter items such that the value of the given key is between the given values. * * @param string $key - * @param array $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @return static */ public function whereBetween($key, $values); @@ -350,7 +434,7 @@ public function whereBetween($key, $values); * Filter items such that the value of the given key is not between the given values. * * @param string $key - * @param array $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @return static */ public function whereNotBetween($key, $values); @@ -359,7 +443,7 @@ public function whereNotBetween($key, $values); * Filter items by the given key value pair. * * @param string $key - * @param mixed $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @param bool $strict * @return static */ @@ -369,25 +453,29 @@ public function whereNotIn($key, $values, $strict = false); * Filter items by the given key value pair using strict comparison. * * @param string $key - * @param mixed $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @return static */ public function whereNotInStrict($key, $values); /** - * Filter the items, removing any items that don't match the given type. + * Filter the items, removing any items that don't match the given type(s). * - * @param string $type - * @return static + * @template TWhereInstanceOf + * + * @param class-string|array> $type + * @return static */ public function whereInstanceOf($type); /** * Get the first item from the enumerable passing the given truth test. * - * @param callable|null $callback - * @param mixed $default - * @return mixed + * @template TFirstDefault + * + * @param (callable(TValue,TKey): bool)|null $callback + * @param TFirstDefault|(\Closure(): TFirstDefault) $default + * @return TValue|TFirstDefault */ public function first(callable $callback = null, $default = null); @@ -397,56 +485,74 @@ public function first(callable $callback = null, $default = null); * @param string $key * @param mixed $operator * @param mixed $value - * @return mixed + * @return TValue|null */ public function firstWhere($key, $operator = null, $value = null); /** - * Flip the values with their keys. + * Get a flattened array of the items in the collection. * + * @param int $depth * @return static */ + public function flatten($depth = INF); + + /** + * Flip the values with their keys. + * + * @return static + */ public function flip(); /** * Get an item from the collection by key. * - * @param mixed $key - * @param mixed $default - * @return mixed + * @template TGetDefault + * + * @param TKey $key + * @param TGetDefault|(\Closure(): TGetDefault) $default + * @return TValue|TGetDefault */ public function get($key, $default = null); /** * Group an associative array by a field or using a callback. * - * @param array|callable|string $groupBy + * @param (callable(TValue, TKey): array-key)|array|string $groupBy * @param bool $preserveKeys - * @return static + * @return static> */ public function groupBy($groupBy, $preserveKeys = false); /** * Key an associative array by a field or using a callback. * - * @param callable|string $keyBy - * @return static + * @param (callable(TValue, TKey): array-key)|array|string $keyBy + * @return static */ public function keyBy($keyBy); /** * Determine if an item exists in the collection by key. * - * @param mixed $key + * @param TKey|array $key * @return bool */ public function has($key); + /** + * Determine if any of the keys exist in the collection. + * + * @param mixed $key + * @return bool + */ + public function hasAny($key); + /** * Concatenate values of a given key as a string. * * @param string $value - * @param string $glue + * @param string|null $glue * @return string */ public function implode($value, $glue = null); @@ -454,7 +560,7 @@ public function implode($value, $glue = null); /** * Intersect the collection with the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function intersect($items); @@ -462,7 +568,7 @@ public function intersect($items); /** * Intersect the collection with the given items by key. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function intersectByKeys($items); @@ -481,6 +587,13 @@ public function isEmpty(); */ public function isNotEmpty(); + /** + * Determine if the collection contains a single item. + * + * @return bool + */ + public function containsOneItem(); + /** * Join all items from the collection using a string. The final items can use a separate glue string. * @@ -493,24 +606,28 @@ public function join($glue, $finalGlue = ''); /** * Get the keys of the collection items. * - * @return static + * @return static */ public function keys(); /** * Get the last item from the collection. * - * @param callable|null $callback - * @param mixed $default - * @return mixed + * @template TLastDefault + * + * @param (callable(TValue, TKey): bool)|null $callback + * @param TLastDefault|(\Closure(): TLastDefault) $default + * @return TValue|TLastDefault */ public function last(callable $callback = null, $default = null); /** * Run a map over each of the items. * - * @param callable $callback - * @return static + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return static */ public function map(callable $callback); @@ -527,8 +644,11 @@ public function mapSpread(callable $callback); * * The callback should return an associative array with a single key/value pair. * - * @param callable $callback - * @return static + * @template TMapToDictionaryKey of array-key + * @template TMapToDictionaryValue + * + * @param callable(TValue, TKey): array $callback + * @return static> */ public function mapToDictionary(callable $callback); @@ -537,8 +657,11 @@ public function mapToDictionary(callable $callback); * * The callback should return an associative array with a single key/value pair. * - * @param callable $callback - * @return static + * @template TMapToGroupsKey of array-key + * @template TMapToGroupsValue + * + * @param callable(TValue, TKey): array $callback + * @return static> */ public function mapToGroups(callable $callback); @@ -547,31 +670,36 @@ public function mapToGroups(callable $callback); * * The callback should return an associative array with a single key/value pair. * - * @param callable $callback - * @return static + * @template TMapWithKeysKey of array-key + * @template TMapWithKeysValue + * + * @param callable(TValue, TKey): array $callback + * @return static */ public function mapWithKeys(callable $callback); /** * Map a collection and flatten the result by a single level. * - * @param callable $callback - * @return static + * @param callable(TValue, TKey): mixed $callback + * @return static */ public function flatMap(callable $callback); /** * Map the values into a new class. * - * @param string $class - * @return static + * @template TMapIntoValue + * + * @param class-string $class + * @return static */ public function mapInto($class); /** * Merge the collection with the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function merge($items); @@ -579,23 +707,27 @@ public function merge($items); /** * Recursively merge the collection with the given items. * - * @param mixed $items - * @return static + * @template TMergeRecursiveValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items + * @return static */ public function mergeRecursive($items); /** * Create a collection by using this collection for keys and another for its values. * - * @param mixed $values - * @return static + * @template TCombineValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values + * @return static */ public function combine($values); /** * Union the collection with the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function union($items); @@ -603,7 +735,7 @@ public function union($items); /** * Get the min value of a given key. * - * @param callable|string|null $callback + * @param (callable(TValue):mixed)|string|null $callback * @return mixed */ public function min($callback = null); @@ -611,7 +743,7 @@ public function min($callback = null); /** * Get the max value of a given key. * - * @param callable|string|null $callback + * @param (callable(TValue):mixed)|string|null $callback * @return mixed */ public function max($callback = null); @@ -628,7 +760,7 @@ public function nth($step, $offset = 0); /** * Get the items with the specified keys. * - * @param mixed $keys + * @param \Illuminate\Support\Enumerable|array|string $keys * @return static */ public function only($keys); @@ -645,17 +777,17 @@ public function forPage($page, $perPage); /** * Partition the collection into two arrays using the given callback or key. * - * @param callable|string $key + * @param (callable(TValue, TKey): bool)|TValue|string $key * @param mixed $operator * @param mixed $value - * @return static + * @return static, static> */ public function partition($key, $operator = null, $value = null); /** * Push all of the given items onto the collection. * - * @param iterable $source + * @param iterable $source * @return static */ public function concat($source); @@ -664,7 +796,7 @@ public function concat($source); * Get one or a specified number of items randomly from the collection. * * @param int|null $number - * @return static|mixed + * @return static|TValue * * @throws \InvalidArgumentException */ @@ -673,16 +805,30 @@ public function random($number = null); /** * Reduce the collection to a single value. * - * @param callable $callback - * @param mixed $initial - * @return mixed + * @template TReduceInitial + * @template TReduceReturnType + * + * @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback + * @param TReduceInitial $initial + * @return TReduceReturnType */ public function reduce(callable $callback, $initial = null); + /** + * Reduce the collection to multiple aggregate values. + * + * @param callable $callback + * @param mixed ...$initial + * @return array + * + * @throws \UnexpectedValueException + */ + public function reduceSpread(callable $callback, ...$initial); + /** * Replace the collection items with the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function replace($items); @@ -690,7 +836,7 @@ public function replace($items); /** * Recursively replace the collection items with the given items. * - * @param mixed $items + * @param \Illuminate\Contracts\Support\Arrayable|iterable $items * @return static */ public function replaceRecursive($items); @@ -705,20 +851,29 @@ public function reverse(); /** * Search the collection for a given value and return the corresponding key if successful. * - * @param mixed $value + * @param TValue|callable(TValue,TKey): bool $value * @param bool $strict - * @return mixed + * @return TKey|bool */ public function search($value, $strict = false); /** * Shuffle the items in the collection. * - * @param int $seed + * @param int|null $seed * @return static */ public function shuffle($seed = null); + /** + * Create chunks representing a "sliding window" view of the items in the collection. + * + * @param int $size + * @param int $step + * @return static + */ + public function sliding($size = 2, $step = 1); + /** * Skip the first {$count} items. * @@ -727,11 +882,27 @@ public function shuffle($seed = null); */ public function skip($count); + /** + * Skip items in the collection until the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function skipUntil($value); + + /** + * Skip items in the collection while the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function skipWhile($value); + /** * Get a slice of items from the enumerable. * * @param int $offset - * @param int $length + * @param int|null $length * @return static */ public function slice($offset, $length = null); @@ -740,30 +911,79 @@ public function slice($offset, $length = null); * Split a collection into a certain number of groups. * * @param int $numberOfGroups - * @return static + * @return static */ public function split($numberOfGroups); + /** + * Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception. + * + * @param (callable(TValue, TKey): bool)|string $key + * @param mixed $operator + * @param mixed $value + * @return TValue + * + * @throws \Illuminate\Support\ItemNotFoundException + * @throws \Illuminate\Support\MultipleItemsFoundException + */ + public function sole($key = null, $operator = null, $value = null); + + /** + * Get the first item in the collection but throw an exception if no matching items exist. + * + * @param (callable(TValue, TKey): bool)|string $key + * @param mixed $operator + * @param mixed $value + * @return TValue + * + * @throws \Illuminate\Support\ItemNotFoundException + */ + public function firstOrFail($key = null, $operator = null, $value = null); + /** * Chunk the collection into chunks of the given size. * * @param int $size - * @return static + * @return static */ public function chunk($size); + /** + * Chunk the collection into chunks with a callback. + * + * @param callable(TValue, TKey, static): bool $callback + * @return static> + */ + public function chunkWhile(callable $callback); + + /** + * Split a collection into a certain number of groups, and fill the first groups completely. + * + * @param int $numberOfGroups + * @return static + */ + public function splitIn($numberOfGroups); + /** * Sort through each item with a callback. * - * @param callable|null $callback + * @param (callable(TValue, TValue): int)|null|int $callback * @return static */ - public function sort(callable $callback = null); + public function sort($callback = null); + + /** + * Sort items in descending order. + * + * @param int $options + * @return static + */ + public function sortDesc($options = SORT_REGULAR); /** * Sort the collection using the given callback. * - * @param callable|string $callback + * @param array|(callable(TValue, TKey): mixed)|string $callback * @param int $options * @param bool $descending * @return static @@ -773,7 +993,7 @@ public function sortBy($callback, $options = SORT_REGULAR, $descending = false); /** * Sort the collection in descending order using the given callback. * - * @param callable|string $callback + * @param array|(callable(TValue, TKey): mixed)|string $callback * @param int $options * @return static */ @@ -791,15 +1011,23 @@ public function sortKeys($options = SORT_REGULAR, $descending = false); /** * Sort the collection keys in descending order. * - * @param int $options + * @param int $options * @return static */ public function sortKeysDesc($options = SORT_REGULAR); + /** + * Sort the collection keys using a callback. + * + * @param callable(TKey, TKey): int $callback + * @return static + */ + public function sortKeysUsing(callable $callback); + /** * Get the sum of the given values. * - * @param callable|string|null $callback + * @param (callable(TValue): mixed)|string|null $callback * @return mixed */ public function sum($callback = null); @@ -812,10 +1040,26 @@ public function sum($callback = null); */ public function take($limit); + /** + * Take items in the collection until the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function takeUntil($value); + + /** + * Take items in the collection while the given condition is met. + * + * @param TValue|callable(TValue,TKey): bool $value + * @return static + */ + public function takeWhile($value); + /** * Pass the collection to the given callback and then return it. * - * @param callable $callback + * @param callable(TValue): mixed $callback * @return $this */ public function tap(callable $callback); @@ -823,32 +1067,57 @@ public function tap(callable $callback); /** * Pass the enumerable to the given callback and return the result. * - * @param callable $callback - * @return mixed + * @template TPipeReturnType + * + * @param callable($this): TPipeReturnType $callback + * @return TPipeReturnType */ public function pipe(callable $callback); + /** + * Pass the collection into a new class. + * + * @param class-string $class + * @return mixed + */ + public function pipeInto($class); + + /** + * Pass the collection through a series of callable pipes and return the result. + * + * @param array $pipes + * @return mixed + */ + public function pipeThrough($pipes); + /** * Get the values of a given key. * - * @param string|array $value + * @param string|array $value * @param string|null $key - * @return static + * @return static */ public function pluck($value, $key = null); /** * Create a collection of all elements that do not pass a given truth test. * - * @param callable|mixed $callback + * @param (callable(TValue, TKey): bool)|bool $callback * @return static */ public function reject($callback = true); + /** + * Convert a flatten "dot" notation array into an expanded array. + * + * @return static + */ + public function undot(); + /** * Return only unique items from the collection array. * - * @param string|callable|null $key + * @param (callable(TValue, TKey): mixed)|string|null $key * @param bool $strict * @return static */ @@ -857,7 +1126,7 @@ public function unique($key = null, $strict = false); /** * Return only unique items from the collection array using strict comparison. * - * @param string|callable|null $key + * @param (callable(TValue, TKey): mixed)|string|null $key * @return static */ public function uniqueStrict($key = null); @@ -865,34 +1134,93 @@ public function uniqueStrict($key = null); /** * Reset the keys on the underlying array. * - * @return static + * @return static */ public function values(); /** * Pad collection to the specified length with a value. * + * @template TPadValue + * * @param int $size - * @param mixed $value - * @return static + * @param TPadValue $value + * @return static */ public function pad($size, $value); /** - * Count the number of items in the collection using a given truth test. + * Get the values iterator. * - * @param callable|null $callback - * @return static + * @return \Traversable + */ + public function getIterator(): Traversable; + + /** + * Count the number of items in the collection. + * + * @return int + */ + public function count(): int; + + /** + * Count the number of items in the collection by a field or using a callback. + * + * @param (callable(TValue, TKey): mixed)|string|null $countBy + * @return static */ public function countBy($callback = null); + /** + * Zip the collection together with one or more arrays. + * + * e.g. new Collection([1, 2, 3])->zip([4, 5, 6]); + * => [[1, 4], [2, 5], [3, 6]] + * + * @template TZipValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable ...$items + * @return static> + */ + public function zip($items); + /** * Collect the values into a collection. * - * @return \Illuminate\Support\Collection + * @return \Illuminate\Support\Collection */ public function collect(); + /** + * Get the collection of items as a plain array. + * + * @return array + */ + public function toArray(); + + /** + * Convert the object into something JSON serializable. + * + * @return mixed + */ + public function jsonSerialize(): mixed; + + /** + * Get the collection of items as JSON. + * + * @param int $options + * @return string + */ + public function toJson($options = 0); + + /** + * Get a CachingIterator instance. + * + * @param int $flags + * @return \CachingIterator + */ + public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING); + /** * Convert the collection to its string representation. * @@ -900,6 +1228,14 @@ public function collect(); */ public function __toString(); + /** + * Indicate that the model's string representation should be escaped when __toString is invoked. + * + * @param bool $escape + * @return $this + */ + public function escapeWhenCastingToString($escape = true); + /** * Add a method to the list of proxied methods. * diff --git a/src/Support/Facades/Blade.php b/src/Support/Facades/Blade.php deleted file mode 100644 index 2fa6499..0000000 --- a/src/Support/Facades/Blade.php +++ /dev/null @@ -1,22 +0,0 @@ -js = $this->convertDataToJavaScriptExpression($data, $flags, $depth); - } - - /** - * Create a new JavaScript string from the given data. - * - * @param mixed $data - * @param int $flags - * @param int $depth - * @return static - * - * @throws \JsonException - */ - public static function from($data, $flags = 0, $depth = 512) - { - return new static($data, $flags, $depth); - } - - /** - * Convert the given data to a JavaScript expression. - * - * @param mixed $data - * @param int $flags - * @param int $depth - * @return string - * - * @throws \JsonException - */ - protected function convertDataToJavaScriptExpression($data, $flags = 0, $depth = 512) - { - if ($data instanceof self) { - return $data->toHtml(); - } - - $json = $this->jsonEncode($data, $flags, $depth); - - if (is_string($data)) { - return "'".substr($json, 1, -1)."'"; - } - - return $this->convertJsonToJavaScriptExpression($json, $flags); - } - - /** - * Encode the given data as JSON. - * - * @param mixed $data - * @param int $flags - * @param int $depth - * @return string - * - * @throws \JsonException - */ - protected function jsonEncode($data, $flags = 0, $depth = 512) - { - if ($data instanceof Jsonable) { - return $data->toJson($flags | static::REQUIRED_FLAGS); - } - - if ($data instanceof Arrayable && ! ($data instanceof JsonSerializable)) { - $data = $data->toArray(); - } - - return json_encode($data, $flags | static::REQUIRED_FLAGS, $depth); - } - - /** - * Convert the given JSON to a JavaScript expression. - * - * @param string $json - * @param int $flags - * @return string - * - * @throws \JsonException - */ - protected function convertJsonToJavaScriptExpression($json, $flags = 0) - { - if ($json === '[]' || $json === '{}') { - return $json; - } - - if (Str::startsWith($json, ['"', '{', '['])) { - return "JSON.parse('".substr(json_encode($json, $flags | static::REQUIRED_FLAGS), 1, -1)."')"; - } - - return $json; - } - - /** - * Get the string representation of the data for use in HTML. - * - * @return string - */ - public function toHtml() - { - return $this->js; - } - - /** - * Get the string representation of the data for use in HTML. - * - * @return string - */ - public function __toString() - { - return $this->toHtml(); - } -} diff --git a/src/Support/Pluralizer.php b/src/Support/Pluralizer.php deleted file mode 100644 index 01cba35..0000000 --- a/src/Support/Pluralizer.php +++ /dev/null @@ -1,121 +0,0 @@ - $val) { - $value = str_replace($val, $key, $value); - } - - return preg_replace('/[^\x20-\x7E]/u', '', $value); - } - /** * Get the portion of a string before a given value. * @@ -271,59 +238,6 @@ public static function isJson($value) return true; } - /** - * Determine if a given value is a valid URL. - * - * @param mixed $value - * @return bool - */ - public static function isUrl($value) - { - if (! is_string($value)) { - return false; - } - - /* - * This pattern is derived from Symfony\Component\Validator\Constraints\UrlValidator (5.0.7). - * - * (c) Fabien Potencier http://symfony.com - */ - $pattern = '~^ - (aaa|aaas|about|acap|acct|acd|acr|adiumxtra|adt|afp|afs|aim|amss|android|appdata|apt|ark|attachment|aw|barion|beshare|bitcoin|bitcoincash|blob|bolo|browserext|calculator|callto|cap|cast|casts|chrome|chrome-extension|cid|coap|coap\+tcp|coap\+ws|coaps|coaps\+tcp|coaps\+ws|com-eventbrite-attendee|content|conti|crid|cvs|dab|data|dav|diaspora|dict|did|dis|dlna-playcontainer|dlna-playsingle|dns|dntp|dpp|drm|drop|dtn|dvb|ed2k|elsi|example|facetime|fax|feed|feedready|file|filesystem|finger|first-run-pen-experience|fish|fm|ftp|fuchsia-pkg|geo|gg|git|gizmoproject|go|gopher|graph|gtalk|h323|ham|hcap|hcp|http|https|hxxp|hxxps|hydrazone|iax|icap|icon|im|imap|info|iotdisco|ipn|ipp|ipps|irc|irc6|ircs|iris|iris\.beep|iris\.lwz|iris\.xpc|iris\.xpcs|isostore|itms|jabber|jar|jms|keyparc|lastfm|ldap|ldaps|leaptofrogans|lorawan|lvlt|magnet|mailserver|mailto|maps|market|message|mid|mms|modem|mongodb|moz|ms-access|ms-browser-extension|ms-calculator|ms-drive-to|ms-enrollment|ms-excel|ms-eyecontrolspeech|ms-gamebarservices|ms-gamingoverlay|ms-getoffice|ms-help|ms-infopath|ms-inputapp|ms-lockscreencomponent-config|ms-media-stream-id|ms-mixedrealitycapture|ms-mobileplans|ms-officeapp|ms-people|ms-project|ms-powerpoint|ms-publisher|ms-restoretabcompanion|ms-screenclip|ms-screensketch|ms-search|ms-search-repair|ms-secondary-screen-controller|ms-secondary-screen-setup|ms-settings|ms-settings-airplanemode|ms-settings-bluetooth|ms-settings-camera|ms-settings-cellular|ms-settings-cloudstorage|ms-settings-connectabledevices|ms-settings-displays-topology|ms-settings-emailandaccounts|ms-settings-language|ms-settings-location|ms-settings-lock|ms-settings-nfctransactions|ms-settings-notifications|ms-settings-power|ms-settings-privacy|ms-settings-proximity|ms-settings-screenrotation|ms-settings-wifi|ms-settings-workplace|ms-spd|ms-sttoverlay|ms-transit-to|ms-useractivityset|ms-virtualtouchpad|ms-visio|ms-walk-to|ms-whiteboard|ms-whiteboard-cmd|ms-word|msnim|msrp|msrps|mss|mtqp|mumble|mupdate|mvn|news|nfs|ni|nih|nntp|notes|ocf|oid|onenote|onenote-cmd|opaquelocktoken|openpgp4fpr|pack|palm|paparazzi|payto|pkcs11|platform|pop|pres|prospero|proxy|pwid|psyc|pttp|qb|query|redis|rediss|reload|res|resource|rmi|rsync|rtmfp|rtmp|rtsp|rtsps|rtspu|s3|secondlife|service|session|sftp|sgn|shttp|sieve|simpleledger|sip|sips|skype|smb|sms|smtp|snews|snmp|soap\.beep|soap\.beeps|soldat|spiffe|spotify|ssh|steam|stun|stuns|submit|svn|tag|teamspeak|tel|teliaeid|telnet|tftp|tg|things|thismessage|tip|tn3270|tool|ts3server|turn|turns|tv|udp|unreal|urn|ut2004|v-event|vemmi|ventrilo|videotex|vnc|view-source|wais|webcal|wpid|ws|wss|wtai|wyciwyg|xcon|xcon-userid|xfire|xmlrpc\.beep|xmlrpc\.beeps|xmpp|xri|ymsgr|z39\.50|z39\.50r|z39\.50s):// # protocol - (((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%[0-9A-Fa-f]{2})+)@)? # basic auth - ( - ([\pL\pN\pS\-\_\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name - | # or - \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address - | # or - \[ - (?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::)))) - \] # an IPv6 address - ) - (:[0-9]+)? # a port (optional) - (?:/ (?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})* )* # a path - (?:\? (?:[\pL\pN\-._\~!$&\'\[\]()*+,;=:@/?]|%[0-9A-Fa-f]{2})* )? # a query (optional) - (?:\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})* )? # a fragment (optional) - $~ixu'; - - return preg_match($pattern, $value) > 0; - } - - /** - * Determine if a given value is a valid UUID. - * - * @param mixed $value - * @return bool - */ - public static function isUuid($value) - { - if (! is_string($value)) { - return false; - } - - return preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $value) > 0; - } - /** * Convert a string to kebab case. * @@ -410,34 +324,6 @@ public static function parseCallback($callback, $default = null) return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default]; } - /** - * Get the plural form of an English word. - * - * @param string $value - * @param int $count - * @return string - */ - public static function plural($value, $count = 2) - { - return Pluralizer::plural($value, $count); - } - - /** - * Pluralize the last word of an English, studly caps case string. - * - * @param string $value - * @param int $count - * @return string - */ - public static function pluralStudly($value, $count = 2) - { - $parts = preg_split('/(.)(?=[A-Z])/u', $value, -1, PREG_SPLIT_DELIM_CAPTURE); - - $lastWord = array_pop($parts); - - return implode('', $parts).self::plural($lastWord, $count); - } - /** * Generate a more truly "random" alpha-numeric string. * @@ -586,17 +472,6 @@ public static function title($value) return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8'); } - /** - * Get the singular form of an English word. - * - * @param string $value - * @return string - */ - public static function singular($value) - { - return Pluralizer::singular($value); - } - /** * Generate a URL friendly "slug" from a given string. * @@ -798,297 +673,4 @@ public static function wordCount($string, $characters = null) { return str_word_count($string, 0, $characters); } - - /** - * Generate a UUID (version 4). - * - * @return \Ramsey\Uuid\UuidInterface - */ - public static function uuid() - { - return static::$uuidFactory - ? call_user_func(static::$uuidFactory) - : Uuid::uuid4(); - } - - /** - * Generate a time-ordered UUID (version 4). - * - * @return \Ramsey\Uuid\UuidInterface - */ - public static function orderedUuid() - { - if (static::$uuidFactory) { - return call_user_func(static::$uuidFactory); - } - - $factory = new UuidFactory(); - - $factory->setRandomGenerator(new CombGenerator( - $factory->getRandomGenerator(), - $factory->getNumberConverter() - )); - - $factory->setCodec(new TimestampFirstCombCodec( - $factory->getUuidBuilder() - )); - - return $factory->uuid4(); - } - - /** - * Set the callable that will be used to generate UUIDs. - * - * @param callable $factory - * @return void - */ - public static function createUuidsUsing(callable $factory = null) - { - static::$uuidFactory = $factory; - } - - /** - * Set the sequence that will be used to generate UUIDs. - * - * @param array $sequence - * @param callable|null $whenMissing - * @return void - */ - public static function createUuidsUsingSequence(array $sequence, $whenMissing = null) - { - $next = 0; - - $whenMissing ??= function () use (&$next) { - $factoryCache = static::$uuidFactory; - - static::$uuidFactory = null; - - $uuid = static::uuid(); - - static::$uuidFactory = $factoryCache; - - $next++; - - return $uuid; - }; - - static::createUuidsUsing(function () use (&$next, $sequence, $whenMissing) { - if (array_key_exists($next, $sequence)) { - return $sequence[$next++]; - } - - return $whenMissing(); - }); - } - - /** - * Always return the same UUID when generating new UUIDs. - * - * @param \Closure|null $callback - * @return \Ramsey\Uuid\UuidInterface - */ - public static function freezeUuids(Closure $callback = null) - { - $uuid = Str::uuid(); - - Str::createUuidsUsing(fn () => $uuid); - - if ($callback !== null) { - try { - $callback($uuid); - } finally { - Str::createUuidsNormally(); - } - } - - return $uuid; - } - - /** - * Indicate that UUIDs should be created normally and not using a custom factory. - * - * @return void - */ - public static function createUuidsNormally() - { - static::$uuidFactory = null; - } - - /** - * Returns the replacements for the ascii method. - * - * Note: Adapted from Stringy\Stringy. - * - * @see https://github.com/danielstjules/Stringy/blob/3.1.0/LICENSE.txt - * - * @return array - */ - protected static function charsArray() - { - static $charsArray; - - if (isset($charsArray)) { - return $charsArray; - } - - return $charsArray = [ - '0' => ['°', '₀', '۰', '0'], - '1' => ['¹', '₁', '۱', '1'], - '2' => ['²', '₂', '۲', '2'], - '3' => ['³', '₃', '۳', '3'], - '4' => ['⁴', '₄', '۴', '٤', '4'], - '5' => ['⁵', '₅', '۵', '٥', '5'], - '6' => ['⁶', '₆', '۶', '٦', '6'], - '7' => ['⁷', '₇', '۷', '7'], - '8' => ['⁸', '₈', '۸', '8'], - '9' => ['⁹', '₉', '۹', '9'], - 'a' => ['à', 'á', 'ả', 'ã', 'ạ', 'ă', 'ắ', 'ằ', 'ẳ', 'ẵ', 'ặ', 'â', 'ấ', 'ầ', 'ẩ', 'ẫ', 'ậ', 'ā', 'ą', 'å', 'α', 'ά', 'ἀ', 'ἁ', 'ἂ', 'ἃ', 'ἄ', 'ἅ', 'ἆ', 'ἇ', 'ᾀ', 'ᾁ', 'ᾂ', 'ᾃ', 'ᾄ', 'ᾅ', 'ᾆ', 'ᾇ', 'ὰ', 'ά', 'ᾰ', 'ᾱ', 'ᾲ', 'ᾳ', 'ᾴ', 'ᾶ', 'ᾷ', 'а', 'أ', 'အ', 'ာ', 'ါ', 'ǻ', 'ǎ', 'ª', 'ა', 'अ', 'ا', 'a', 'ä', 'א'], - 'b' => ['б', 'β', 'ب', 'ဗ', 'ბ', 'b', 'ב'], - 'c' => ['ç', 'ć', 'č', 'ĉ', 'ċ', 'c'], - 'd' => ['ď', 'ð', 'đ', 'ƌ', 'ȡ', 'ɖ', 'ɗ', 'ᵭ', 'ᶁ', 'ᶑ', 'д', 'δ', 'د', 'ض', 'ဍ', 'ဒ', 'დ', 'd', 'ד'], - 'e' => ['é', 'è', 'ẻ', 'ẽ', 'ẹ', 'ê', 'ế', 'ề', 'ể', 'ễ', 'ệ', 'ë', 'ē', 'ę', 'ě', 'ĕ', 'ė', 'ε', 'έ', 'ἐ', 'ἑ', 'ἒ', 'ἓ', 'ἔ', 'ἕ', 'ὲ', 'έ', 'е', 'ё', 'э', 'є', 'ə', 'ဧ', 'ေ', 'ဲ', 'ე', 'ए', 'إ', 'ئ', 'e'], - 'f' => ['ф', 'φ', 'ف', 'ƒ', 'ფ', 'f', 'פ', 'ף'], - 'g' => ['ĝ', 'ğ', 'ġ', 'ģ', 'г', 'ґ', 'γ', 'ဂ', 'გ', 'گ', 'g', 'ג'], - 'h' => ['ĥ', 'ħ', 'η', 'ή', 'ح', 'ه', 'ဟ', 'ှ', 'ჰ', 'h', 'ה'], - 'i' => ['í', 'ì', 'ỉ', 'ĩ', 'ị', 'î', 'ï', 'ī', 'ĭ', 'į', 'ı', 'ι', 'ί', 'ϊ', 'ΐ', 'ἰ', 'ἱ', 'ἲ', 'ἳ', 'ἴ', 'ἵ', 'ἶ', 'ἷ', 'ὶ', 'ί', 'ῐ', 'ῑ', 'ῒ', 'ΐ', 'ῖ', 'ῗ', 'і', 'ї', 'и', 'ဣ', 'ိ', 'ီ', 'ည်', 'ǐ', 'ი', 'इ', 'ی', 'i', 'י'], - 'j' => ['ĵ', 'ј', 'Ј', 'ჯ', 'ج', 'j'], - 'k' => ['ķ', 'ĸ', 'к', 'κ', 'Ķ', 'ق', 'ك', 'က', 'კ', 'ქ', 'ک', 'k', 'ק'], - 'l' => ['ł', 'ľ', 'ĺ', 'ļ', 'ŀ', 'л', 'λ', 'ل', 'လ', 'ლ', 'l', 'ל'], - 'm' => ['м', 'μ', 'م', 'မ', 'მ', 'm', 'מ', 'ם'], - 'n' => ['ñ', 'ń', 'ň', 'ņ', 'ʼn', 'ŋ', 'ν', 'н', 'ن', 'န', 'ნ', 'n', 'נ'], - 'o' => ['ó', 'ò', 'ỏ', 'õ', 'ọ', 'ô', 'ố', 'ồ', 'ổ', 'ỗ', 'ộ', 'ơ', 'ớ', 'ờ', 'ở', 'ỡ', 'ợ', 'ø', 'ō', 'ő', 'ŏ', 'ο', 'ὀ', 'ὁ', 'ὂ', 'ὃ', 'ὄ', 'ὅ', 'ὸ', 'ό', 'о', 'و', 'ို', 'ǒ', 'ǿ', 'º', 'ო', 'ओ', 'o', 'ö'], - 'p' => ['п', 'π', 'ပ', 'პ', 'پ', 'p', 'פ', 'ף'], - 'q' => ['ყ', 'q'], - 'r' => ['ŕ', 'ř', 'ŗ', 'р', 'ρ', 'ر', 'რ', 'r', 'ר'], - 's' => ['ś', 'š', 'ş', 'с', 'σ', 'ș', 'ς', 'س', 'ص', 'စ', 'ſ', 'ს', 's', 'ס'], - 't' => ['ť', 'ţ', 'т', 'τ', 'ț', 'ت', 'ط', 'ဋ', 'တ', 'ŧ', 'თ', 'ტ', 't', 'ת'], - 'u' => ['ú', 'ù', 'ủ', 'ũ', 'ụ', 'ư', 'ứ', 'ừ', 'ử', 'ữ', 'ự', 'û', 'ū', 'ů', 'ű', 'ŭ', 'ų', 'µ', 'у', 'ဉ', 'ု', 'ူ', 'ǔ', 'ǖ', 'ǘ', 'ǚ', 'ǜ', 'უ', 'उ', 'u', 'ў', 'ü'], - 'v' => ['в', 'ვ', 'ϐ', 'v', 'ו'], - 'w' => ['ŵ', 'ω', 'ώ', 'ဝ', 'ွ', 'w'], - 'x' => ['χ', 'ξ', 'x'], - 'y' => ['ý', 'ỳ', 'ỷ', 'ỹ', 'ỵ', 'ÿ', 'ŷ', 'й', 'ы', 'υ', 'ϋ', 'ύ', 'ΰ', 'ي', 'ယ', 'y'], - 'z' => ['ź', 'ž', 'ż', 'з', 'ζ', 'ز', 'ဇ', 'ზ', 'z', 'ז'], - 'aa' => ['ع', 'आ', 'آ'], - 'ae' => ['æ', 'ǽ'], - 'ai' => ['ऐ'], - 'ch' => ['ч', 'ჩ', 'ჭ', 'چ'], - 'dj' => ['ђ', 'đ'], - 'dz' => ['џ', 'ძ', 'דז'], - 'ei' => ['ऍ'], - 'gh' => ['غ', 'ღ'], - 'ii' => ['ई'], - 'ij' => ['ij'], - 'kh' => ['х', 'خ', 'ხ'], - 'lj' => ['љ'], - 'nj' => ['њ'], - 'oe' => ['ö', 'œ', 'ؤ'], - 'oi' => ['ऑ'], - 'oii' => ['ऒ'], - 'ps' => ['ψ'], - 'sh' => ['ш', 'შ', 'ش', 'ש'], - 'shch' => ['щ'], - 'ss' => ['ß'], - 'sx' => ['ŝ'], - 'th' => ['þ', 'ϑ', 'θ', 'ث', 'ذ', 'ظ'], - 'ts' => ['ц', 'ც', 'წ'], - 'ue' => ['ü'], - 'uu' => ['ऊ'], - 'ya' => ['я'], - 'yu' => ['ю'], - 'zh' => ['ж', 'ჟ', 'ژ'], - '(c)' => ['©'], - 'A' => ['Á', 'À', 'Ả', 'Ã', 'Ạ', 'Ă', 'Ắ', 'Ằ', 'Ẳ', 'Ẵ', 'Ặ', 'Â', 'Ấ', 'Ầ', 'Ẩ', 'Ẫ', 'Ậ', 'Å', 'Ā', 'Ą', 'Α', 'Ά', 'Ἀ', 'Ἁ', 'Ἂ', 'Ἃ', 'Ἄ', 'Ἅ', 'Ἆ', 'Ἇ', 'ᾈ', 'ᾉ', 'ᾊ', 'ᾋ', 'ᾌ', 'ᾍ', 'ᾎ', 'ᾏ', 'Ᾰ', 'Ᾱ', 'Ὰ', 'Ά', 'ᾼ', 'А', 'Ǻ', 'Ǎ', 'A', 'Ä'], - 'B' => ['Б', 'Β', 'ब', 'B'], - 'C' => ['Ç', 'Ć', 'Č', 'Ĉ', 'Ċ', 'C'], - 'D' => ['Ď', 'Ð', 'Đ', 'Ɖ', 'Ɗ', 'Ƌ', 'ᴅ', 'ᴆ', 'Д', 'Δ', 'D'], - 'E' => ['É', 'È', 'Ẻ', 'Ẽ', 'Ẹ', 'Ê', 'Ế', 'Ề', 'Ể', 'Ễ', 'Ệ', 'Ë', 'Ē', 'Ę', 'Ě', 'Ĕ', 'Ė', 'Ε', 'Έ', 'Ἐ', 'Ἑ', 'Ἒ', 'Ἓ', 'Ἔ', 'Ἕ', 'Έ', 'Ὲ', 'Е', 'Ё', 'Э', 'Є', 'Ə', 'E'], - 'F' => ['Ф', 'Φ', 'F'], - 'G' => ['Ğ', 'Ġ', 'Ģ', 'Г', 'Ґ', 'Γ', 'G'], - 'H' => ['Η', 'Ή', 'Ħ', 'H'], - 'I' => ['Í', 'Ì', 'Ỉ', 'Ĩ', 'Ị', 'Î', 'Ï', 'Ī', 'Ĭ', 'Į', 'İ', 'Ι', 'Ί', 'Ϊ', 'Ἰ', 'Ἱ', 'Ἳ', 'Ἴ', 'Ἵ', 'Ἶ', 'Ἷ', 'Ῐ', 'Ῑ', 'Ὶ', 'Ί', 'И', 'І', 'Ї', 'Ǐ', 'ϒ', 'I'], - 'J' => ['J'], - 'K' => ['К', 'Κ', 'K'], - 'L' => ['Ĺ', 'Ł', 'Л', 'Λ', 'Ļ', 'Ľ', 'Ŀ', 'ल', 'L'], - 'M' => ['М', 'Μ', 'M'], - 'N' => ['Ń', 'Ñ', 'Ň', 'Ņ', 'Ŋ', 'Н', 'Ν', 'N'], - 'O' => ['Ó', 'Ò', 'Ỏ', 'Õ', 'Ọ', 'Ô', 'Ố', 'Ồ', 'Ổ', 'Ỗ', 'Ộ', 'Ơ', 'Ớ', 'Ờ', 'Ở', 'Ỡ', 'Ợ', 'Ø', 'Ō', 'Ő', 'Ŏ', 'Ο', 'Ό', 'Ὀ', 'Ὁ', 'Ὂ', 'Ὃ', 'Ὄ', 'Ὅ', 'Ὸ', 'Ό', 'О', 'Ө', 'Ǒ', 'Ǿ', 'O', 'Ö'], - 'P' => ['П', 'Π', 'P'], - 'Q' => ['Q'], - 'R' => ['Ř', 'Ŕ', 'Р', 'Ρ', 'Ŗ', 'R'], - 'S' => ['Ş', 'Ŝ', 'Ș', 'Š', 'Ś', 'С', 'Σ', 'S'], - 'T' => ['Ť', 'Ţ', 'Ŧ', 'Ț', 'Т', 'Τ', 'T'], - 'U' => ['Ú', 'Ù', 'Ủ', 'Ũ', 'Ụ', 'Ư', 'Ứ', 'Ừ', 'Ử', 'Ữ', 'Ự', 'Û', 'Ū', 'Ů', 'Ű', 'Ŭ', 'Ų', 'У', 'Ǔ', 'Ǖ', 'Ǘ', 'Ǚ', 'Ǜ', 'U', 'Ў', 'Ü'], - 'V' => ['В', 'V'], - 'W' => ['Ω', 'Ώ', 'Ŵ', 'W'], - 'X' => ['Χ', 'Ξ', 'X'], - 'Y' => ['Ý', 'Ỳ', 'Ỷ', 'Ỹ', 'Ỵ', 'Ÿ', 'Ῠ', 'Ῡ', 'Ὺ', 'Ύ', 'Ы', 'Й', 'Υ', 'Ϋ', 'Ŷ', 'Y'], - 'Z' => ['Ź', 'Ž', 'Ż', 'З', 'Ζ', 'Z'], - 'AE' => ['Æ', 'Ǽ'], - 'Ch' => ['Ч'], - 'Dj' => ['Ђ'], - 'Dz' => ['Џ'], - 'Gx' => ['Ĝ'], - 'Hx' => ['Ĥ'], - 'Ij' => ['IJ'], - 'Jx' => ['Ĵ'], - 'Kh' => ['Х'], - 'Lj' => ['Љ'], - 'Nj' => ['Њ'], - 'Oe' => ['Œ'], - 'Ps' => ['Ψ'], - 'Sh' => ['Ш', 'ש'], - 'Shch' => ['Щ'], - 'Ss' => ['ẞ'], - 'Th' => ['Þ', 'Θ', 'ת'], - 'Ts' => ['Ц'], - 'Ya' => ['Я', 'יא'], - 'Yu' => ['Ю', 'יו'], - 'Zh' => ['Ж'], - ' ' => ["\xC2\xA0", "\xE2\x80\x80", "\xE2\x80\x81", "\xE2\x80\x82", "\xE2\x80\x83", "\xE2\x80\x84", "\xE2\x80\x85", "\xE2\x80\x86", "\xE2\x80\x87", "\xE2\x80\x88", "\xE2\x80\x89", "\xE2\x80\x8A", "\xE2\x80\xAF", "\xE2\x81\x9F", "\xE3\x80\x80", "\xEF\xBE\xA0"], - ]; - } - - /** - * Returns the language specific replacements for the ascii method. - * - * Note: Adapted from Stringy\Stringy. - * - * @see https://github.com/danielstjules/Stringy/blob/3.1.0/LICENSE.txt - * - * @param string $language - * @return array|null - */ - protected static function languageSpecificCharsArray($language) - { - static $languageSpecific; - - if (! isset($languageSpecific)) { - $languageSpecific = [ - 'bg' => [ - ['х', 'Х', 'щ', 'Щ', 'ъ', 'Ъ', 'ь', 'Ь'], - ['h', 'H', 'sht', 'SHT', 'a', 'А', 'y', 'Y'], - ], - 'da' => [ - ['æ', 'ø', 'å', 'Æ', 'Ø', 'Å'], - ['ae', 'oe', 'aa', 'Ae', 'Oe', 'Aa'], - ], - 'de' => [ - ['ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü'], - ['ae', 'oe', 'ue', 'AE', 'OE', 'UE'], - ], - 'he' => [ - ['א', 'ב', 'ג', 'ד', 'ה', 'ו'], - ['ז', 'ח', 'ט', 'י', 'כ', 'ל'], - ['מ', 'נ', 'ס', 'ע', 'פ', 'צ'], - ['ק', 'ר', 'ש', 'ת', 'ן', 'ץ', 'ך', 'ם', 'ף'], - ], - 'ro' => [ - ['ă', 'â', 'î', 'ș', 'ț', 'Ă', 'Â', 'Î', 'Ș', 'Ț'], - ['a', 'a', 'i', 's', 't', 'A', 'A', 'I', 'S', 'T'], - ], - ]; - } - - return $languageSpecific[$language] ?? null; - } } diff --git a/src/Support/Stringable.php b/src/Support/Stringable.php index 292741c..dc455be 100644 --- a/src/Support/Stringable.php +++ b/src/Support/Stringable.php @@ -5,10 +5,11 @@ use ArrayAccess; use Closure; use Illuminate\Support\Traits\Conditionable; - use JsonSerializable; use Stringable as BaseStringable; +use function Illuminate\Support\collect; + class Stringable implements JsonSerializable, ArrayAccess, BaseStringable { use Conditionable; @@ -235,27 +236,6 @@ public function pipe(callable $callback) return new static($callback($this)); } - /** - * Get the plural form of an English word. - * - * @param int|array|\Countable $count - * @return static - */ - public function plural($count = 2) - { - return new static(Str::plural($this->value, $count)); - } - - /** - * Pluralize the last word of an English, studly caps case string. - * - * @param int|array|\Countable $count - * @return static - */ - public function pluralStudly($count = 2) - { - return new static(Str::pluralStudly($this->value, $count)); - } /** * Repeat the string. diff --git a/src/Support/Traits/Conditionable.php b/src/Support/Traits/Conditionable.php index aa77df5..5dd3108 100644 --- a/src/Support/Traits/Conditionable.php +++ b/src/Support/Traits/Conditionable.php @@ -1,4 +1,5 @@ */ protected static $proxies = [ - 'average', 'avg', 'contains', 'each', 'every', 'filter', 'first', - 'flatMap', 'groupBy', 'keyBy', 'map', 'max', 'min', 'partition', - 'reject', 'some', 'sortBy', 'sortByDesc', 'sum', 'unique', + 'average', + 'avg', + 'contains', + 'doesntContain', + 'each', + 'every', + 'filter', + 'first', + 'flatMap', + 'groupBy', + 'keyBy', + 'map', + 'max', + 'min', + 'partition', + 'reject', + 'skipUntil', + 'skipWhile', + 'some', + 'sortBy', + 'sortByDesc', + 'sum', + 'takeUntil', + 'takeWhile', + 'unique', + 'unless', + 'until', + 'when', ]; /** * Create a new collection instance if the value isn't one already. * - * @param mixed $items - * @return static + * @template TMakeKey of array-key + * @template TMakeValue + * + * @param \Illuminate\Contracts\Support\Arrayable|iterable|null $items + * @return static */ public static function make($items = []) { @@ -62,8 +115,11 @@ public static function make($items = []) /** * Wrap the given value in a collection if applicable. * - * @param mixed $value - * @return static + * @template TWrapKey of array-key + * @template TWrapValue + * + * @param iterable $value + * @return static */ public static function wrap($value) { @@ -75,19 +131,52 @@ public static function wrap($value) /** * Get the underlying items from the given collection if applicable. * - * @param array|static $value - * @return array + * @template TUnwrapKey of array-key + * @template TUnwrapValue + * + * @param array|static $value + * @return array */ public static function unwrap($value) { return $value instanceof Enumerable ? $value->all() : $value; } + /** + * Create a new instance with no items. + * + * @return static + */ + public static function empty() + { + return new static([]); + } + + /** + * Create a new collection by invoking the callback a given amount of times. + * + * @template TTimesValue + * + * @param int $number + * @param (callable(int): TTimesValue)|null $callback + * @return static + */ + public static function times($number, callable $callback = null) + { + if ($number < 1) { + return new static; + } + + return static::range(1, $number) + ->unless($callback == null) + ->map($callback); + } + /** * Alias for the "avg" method. * - * @param callable|string|null $callback - * @return mixed + * @param (callable(TValue): float|int)|string|null $callback + * @return float|int|null */ public function average($callback = null) { @@ -97,7 +186,7 @@ public function average($callback = null) /** * Alias for the "contains" method. * - * @param mixed $key + * @param (callable(TValue, TKey): bool)|TValue|string $key * @param mixed $operator * @param mixed $value * @return bool @@ -110,16 +199,14 @@ public function some($key, $operator = null, $value = null) /** * Determine if an item exists, using strict comparison. * - * @param mixed $key - * @param mixed $value + * @param (callable(TValue): bool)|TValue|array-key $key + * @param TValue|null $value * @return bool */ public function containsStrict($key, $value = null) { if (func_num_args() === 2) { - return $this->contains(function ($item) use ($key, $value) { - return data_get($item, $key) === $value; - }); + return $this->contains(fn ($item) => data_get($item, $key) === $value); } if ($this->useAsCallable($key)) { @@ -139,13 +226,13 @@ public function containsStrict($key, $value = null) * Dump the items and end the script. * * @param mixed ...$args - * @return void + * @return never */ public function dd(...$args) { - call_user_func_array([$this, 'dump'], $args); + $this->dump(...$args); - die(1); + exit(1); } /** @@ -155,8 +242,8 @@ public function dd(...$args) */ public function dump() { - (new static(func_get_args())) - ->push($this) + (new Collection(func_get_args())) + ->push($this->all()) ->each(function ($item) { VarDumper::dump($item); }); @@ -167,7 +254,7 @@ public function dump() /** * Execute a callback over each item. * - * @param callable $callback + * @param callable(TValue, TKey): mixed $callback * @return $this */ public function each(callable $callback) @@ -184,7 +271,7 @@ public function each(callable $callback) /** * Execute a callback over each nested chunk of items. * - * @param callable $callback + * @param callable(...mixed): mixed $callback * @return static */ public function eachSpread(callable $callback) @@ -197,9 +284,9 @@ public function eachSpread(callable $callback) } /** - * Determine if all items pass the given test. + * Determine if all items pass the given truth test. * - * @param string|callable $key + * @param (callable(TValue, TKey): bool)|TValue|string $key * @param mixed $operator * @param mixed $value * @return bool @@ -224,16 +311,32 @@ public function every($key, $operator = null, $value = null) /** * Get the first item by the given key value pair. * - * @param string $key + * @param callable|string $key * @param mixed $operator * @param mixed $value - * @return mixed + * @return TValue|null */ public function firstWhere($key, $operator = null, $value = null) { return $this->first($this->operatorForWhere(...func_get_args())); } + /** + * Get a single key's value from the first matching item in the collection. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function value($key, $default = null) + { + if ($value = $this->firstWhere($key)) { + return data_get($value, $key, $default); + } + + return value($default); + } + /** * Determine if the collection is not empty. * @@ -247,8 +350,10 @@ public function isNotEmpty() /** * Run a map over each nested chunk of items. * - * @param callable $callback - * @return static + * @template TMapSpreadValue + * + * @param callable(mixed): TMapSpreadValue $callback + * @return static */ public function mapSpread(callable $callback) { @@ -264,8 +369,11 @@ public function mapSpread(callable $callback) * * The callback should return an associative array with a single key/value pair. * - * @param callable $callback - * @return static + * @template TMapToGroupsKey of array-key + * @template TMapToGroupsValue + * + * @param callable(TValue, TKey): array $callback + * @return static> */ public function mapToGroups(callable $callback) { @@ -277,8 +385,8 @@ public function mapToGroups(callable $callback) /** * Map a collection and flatten the result by a single level. * - * @param callable $callback - * @return static + * @param callable(TValue, TKey): mixed $callback + * @return static */ public function flatMap(callable $callback) { @@ -288,48 +396,42 @@ public function flatMap(callable $callback) /** * Map the values into a new class. * - * @param string $class - * @return static + * @template TMapIntoValue + * + * @param class-string $class + * @return static */ public function mapInto($class) { - return $this->map(function ($value, $key) use ($class) { - return new $class($value, $key); - }); + return $this->map(fn ($value, $key) => new $class($value, $key)); } /** * Get the min value of a given key. * - * @param callable|string|null $callback + * @param (callable(TValue):mixed)|string|null $callback * @return mixed */ public function min($callback = null) { $callback = $this->valueRetriever($callback); - return $this->map(function ($value) use ($callback) { - return $callback($value); - })->filter(function ($value) { - return ! is_null($value); - })->reduce(function ($result, $value) { - return is_null($result) || $value < $result ? $value : $result; - }); + return $this->map(fn ($value) => $callback($value)) + ->filter(fn ($value) => ! is_null($value)) + ->reduce(fn ($result, $value) => is_null($result) || $value < $result ? $value : $result); } /** * Get the max value of a given key. * - * @param callable|string|null $callback + * @param (callable(TValue):mixed)|string|null $callback * @return mixed */ public function max($callback = null) { $callback = $this->valueRetriever($callback); - return $this->filter(function ($value) { - return ! is_null($value); - })->reduce(function ($result, $item) use ($callback) { + return $this->filter(fn ($value) => ! is_null($value))->reduce(function ($result, $item) use ($callback) { $value = $callback($item); return is_null($result) || $value > $result ? $value : $result; @@ -353,10 +455,10 @@ public function forPage($page, $perPage) /** * Partition the collection into two arrays using the given callback or key. * - * @param callable|string $key - * @param mixed $operator - * @param mixed $value - * @return static + * @param (callable(TValue, TKey): bool)|TValue|string $key + * @param TValue|string|null $operator + * @param TValue|null $value + * @return static, static> */ public function partition($key, $operator = null, $value = null) { @@ -381,49 +483,26 @@ public function partition($key, $operator = null, $value = null) /** * Get the sum of the given values. * - * @param callable|string|null $callback + * @param (callable(TValue): mixed)|string|null $callback * @return mixed */ public function sum($callback = null) { - if (is_null($callback)) { - $callback = function ($value) { - return $value; - }; - } else { - $callback = $this->valueRetriever($callback); - } + $callback = is_null($callback) + ? $this->identity() + : $this->valueRetriever($callback); - return $this->reduce(function ($result, $item) use ($callback) { - return $result + $callback($item); - }, 0); - } - - /** - * Apply the callback if the value is truthy. - * - * @param bool $value - * @param callable $callback - * @param callable $default - * @return static|mixed - */ - public function when($value, callable $callback, callable $default = null) - { - if ($value) { - return $callback($this, $value); - } elseif ($default) { - return $default($this, $value); - } - - return $this; + return $this->reduce(fn ($result, $item) => $result + $callback($item), 0); } /** * Apply the callback if the collection is empty. * - * @param callable $callback - * @param callable $default - * @return static|mixed + * @template TWhenEmptyReturnType + * + * @param (callable($this): TWhenEmptyReturnType) $callback + * @param (callable($this): TWhenEmptyReturnType)|null $default + * @return $this|TWhenEmptyReturnType */ public function whenEmpty(callable $callback, callable $default = null) { @@ -433,34 +512,25 @@ public function whenEmpty(callable $callback, callable $default = null) /** * Apply the callback if the collection is not empty. * - * @param callable $callback - * @param callable $default - * @return static|mixed + * @template TWhenNotEmptyReturnType + * + * @param callable($this): TWhenNotEmptyReturnType $callback + * @param (callable($this): TWhenNotEmptyReturnType)|null $default + * @return $this|TWhenNotEmptyReturnType */ public function whenNotEmpty(callable $callback, callable $default = null) { return $this->when($this->isNotEmpty(), $callback, $default); } - /** - * Apply the callback if the value is falsy. - * - * @param bool $value - * @param callable $callback - * @param callable $default - * @return static|mixed - */ - public function unless($value, callable $callback, callable $default = null) - { - return $this->when(! $value, $callback, $default); - } - /** * Apply the callback unless the collection is empty. * - * @param callable $callback - * @param callable $default - * @return static|mixed + * @template TUnlessEmptyReturnType + * + * @param callable($this): TUnlessEmptyReturnType $callback + * @param (callable($this): TUnlessEmptyReturnType)|null $default + * @return $this|TUnlessEmptyReturnType */ public function unlessEmpty(callable $callback, callable $default = null) { @@ -470,9 +540,11 @@ public function unlessEmpty(callable $callback, callable $default = null) /** * Apply the callback unless the collection is not empty. * - * @param callable $callback - * @param callable $default - * @return static|mixed + * @template TUnlessNotEmptyReturnType + * + * @param callable($this): TUnlessNotEmptyReturnType $callback + * @param (callable($this): TUnlessNotEmptyReturnType)|null $default + * @return $this|TUnlessNotEmptyReturnType */ public function unlessNotEmpty(callable $callback, callable $default = null) { @@ -482,7 +554,7 @@ public function unlessNotEmpty(callable $callback, callable $default = null) /** * Filter items by the given key value pair. * - * @param string $key + * @param callable|string $key * @param mixed $operator * @param mixed $value * @return static @@ -492,6 +564,28 @@ public function where($key, $operator = null, $value = null) return $this->filter($this->operatorForWhere(...func_get_args())); } + /** + * Filter items where the value for the given key is null. + * + * @param string|null $key + * @return static + */ + public function whereNull($key = null) + { + return $this->whereStrict($key, null); + } + + /** + * Filter items where the value for the given key is not null. + * + * @param string|null $key + * @return static + */ + public function whereNotNull($key = null) + { + return $this->where($key, '!==', null); + } + /** * Filter items by the given key value pair using strict comparison. * @@ -508,7 +602,7 @@ public function whereStrict($key, $value) * Filter items by the given key value pair. * * @param string $key - * @param mixed $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @param bool $strict * @return static */ @@ -516,16 +610,14 @@ public function whereIn($key, $values, $strict = false) { $values = $this->getArrayableItems($values); - return $this->filter(function ($item) use ($key, $values, $strict) { - return in_array(data_get($item, $key), $values, $strict); - }); + return $this->filter(fn ($item) => in_array(data_get($item, $key), $values, $strict)); } /** * Filter items by the given key value pair using strict comparison. * * @param string $key - * @param mixed $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @return static */ public function whereInStrict($key, $values) @@ -537,7 +629,7 @@ public function whereInStrict($key, $values) * Filter items such that the value of the given key is between the given values. * * @param string $key - * @param array $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @return static */ public function whereBetween($key, $values) @@ -549,21 +641,21 @@ public function whereBetween($key, $values) * Filter items such that the value of the given key is not between the given values. * * @param string $key - * @param array $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @return static */ public function whereNotBetween($key, $values) { - return $this->filter(function ($item) use ($key, $values) { - return data_get($item, $key) < reset($values) || data_get($item, $key) > end($values); - }); + return $this->filter( + fn ($item) => data_get($item, $key) < reset($values) || data_get($item, $key) > end($values) + ); } /** * Filter items by the given key value pair. * * @param string $key - * @param mixed $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @param bool $strict * @return static */ @@ -571,16 +663,14 @@ public function whereNotIn($key, $values, $strict = false) { $values = $this->getArrayableItems($values); - return $this->reject(function ($item) use ($key, $values, $strict) { - return in_array(data_get($item, $key), $values, $strict); - }); + return $this->reject(fn ($item) => in_array(data_get($item, $key), $values, $strict)); } /** * Filter items by the given key value pair using strict comparison. * * @param string $key - * @param mixed $values + * @param \Illuminate\Contracts\Support\Arrayable|iterable $values * @return static */ public function whereNotInStrict($key, $values) @@ -589,14 +679,26 @@ public function whereNotInStrict($key, $values) } /** - * Filter the items, removing any items that don't match the given type. + * Filter the items, removing any items that don't match the given type(s). * - * @param string $type - * @return static + * @template TWhereInstanceOf + * + * @param class-string|array> $type + * @return static */ public function whereInstanceOf($type) { return $this->filter(function ($value) use ($type) { + if (is_array($type)) { + foreach ($type as $classType) { + if ($value instanceof $classType) { + return true; + } + } + + return false; + } + return $value instanceof $type; }); } @@ -604,8 +706,10 @@ public function whereInstanceOf($type) /** * Pass the collection to the given callback and return the result. * - * @param callable $callback - * @return mixed + * @template TPipeReturnType + * + * @param callable($this): TPipeReturnType $callback + * @return TPipeReturnType */ public function pipe(callable $callback) { @@ -613,22 +717,82 @@ public function pipe(callable $callback) } /** - * Pass the collection to the given callback and then return it. + * Pass the collection into a new class. + * + * @param class-string $class + * @return mixed + */ + public function pipeInto($class) + { + return new $class($this); + } + + /** + * Pass the collection through a series of callable pipes and return the result. + * + * @param array $callbacks + * @return mixed + */ + public function pipeThrough($callbacks) + { + return Collection::make($callbacks)->reduce( + fn ($carry, $callback) => $callback($carry), + $this, + ); + } + + /** + * Reduce the collection to a single value. + * + * @template TReduceInitial + * @template TReduceReturnType + * + * @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback + * @param TReduceInitial $initial + * @return TReduceReturnType + */ + public function reduce(callable $callback, $initial = null) + { + $result = $initial; + + foreach ($this as $key => $value) { + $result = $callback($result, $value, $key); + } + + return $result; + } + + /** + * Reduce the collection to multiple aggregate values. * * @param callable $callback - * @return $this + * @param mixed ...$initial + * @return array + * + * @throws \UnexpectedValueException */ - public function tap(callable $callback) + public function reduceSpread(callable $callback, ...$initial) { - $callback(clone $this); + $result = $initial; - return $this; + foreach ($this as $key => $value) { + $result = call_user_func_array($callback, array_merge($result, [$value, $key])); + + if (! is_array($result)) { + throw new UnexpectedValueException(sprintf( + "%s::reduceSpread expects reducer to return an array, but got a '%s' instead.", + class_basename(static::class), gettype($result) + )); + } + } + + return $result; } /** * Create a collection of all elements that do not pass a given truth test. * - * @param callable|mixed $callback + * @param (callable(TValue, TKey): bool)|bool $callback * @return static */ public function reject($callback = true) @@ -642,10 +806,23 @@ public function reject($callback = true) }); } + /** + * Pass the collection to the given callback and then return it. + * + * @param callable($this): mixed $callback + * @return $this + */ + public function tap(callable $callback) + { + $callback($this); + + return $this; + } + /** * Return only unique items from the collection array. * - * @param string|callable|null $key + * @param (callable(TValue, TKey): mixed)|string|null $key * @param bool $strict * @return static */ @@ -667,7 +844,7 @@ public function unique($key = null, $strict = false) /** * Return only unique items from the collection array using strict comparison. * - * @param string|callable|null $key + * @param (callable(TValue, TKey): mixed)|string|null $key * @return static */ public function uniqueStrict($key = null) @@ -678,7 +855,7 @@ public function uniqueStrict($key = null) /** * Collect the values into a collection. * - * @return \Illuminate\Support\Collection + * @return \Illuminate\Support\Collection */ public function collect() { @@ -688,21 +865,19 @@ public function collect() /** * Get the collection of items as a plain array. * - * @return array + * @return array */ public function toArray() { - return $this->map(function ($value) { - return $value instanceof Arrayable ? $value->toArray() : $value; - })->all(); + return $this->map(fn ($value) => $value instanceof Arrayable ? $value->toArray() : $value)->all(); } /** * Convert the object into something JSON serializable. * - * @return array + * @return array */ - public function jsonSerialize(): mixed + public function jsonSerialize(): array { return array_map(function ($value) { if ($value instanceof JsonSerializable) { @@ -740,32 +915,28 @@ public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING) } /** - * Count the number of items in the collection using a given truth test. + * Convert the collection to its string representation. * - * @param callable|null $callback - * @return static + * @return string */ - public function countBy($callback = null) + public function __toString() { - if (is_null($callback)) { - $callback = function ($value) { - return $value; - }; - } - - return new static($this->groupBy($callback)->map(function ($value) { - return $value->count(); - })); + return $this->escapeWhenCastingToString + ? e($this->toJson()) + : $this->toJson(); } /** - * Convert the collection to its string representation. + * Indicate that the model's string representation should be escaped when __toString is invoked. * - * @return string + * @param bool $escape + * @return $this */ - public function __toString() + public function escapeWhenCastingToString($escape = true) { - return $this->toJson(); + $this->escapeWhenCastingToString = $escape; + + return $this; } /** @@ -800,7 +971,7 @@ public function __get($key) * Results array of items from Collection or Arrayable. * * @param mixed $items - * @return array + * @return array */ protected function getArrayableItems($items) { @@ -816,6 +987,8 @@ protected function getArrayableItems($items) return (array) $items->jsonSerialize(); } elseif ($items instanceof Traversable) { return iterator_to_array($items); + } elseif ($items instanceof UnitEnum) { + return [$items]; } return (array) $items; @@ -824,13 +997,17 @@ protected function getArrayableItems($items) /** * Get an operator checker callback. * - * @param string $key - * @param string $operator + * @param callable|string $key + * @param string|null $operator * @param mixed $value * @return \Closure */ protected function operatorForWhere($key, $operator = null, $value = null) { + if ($this->useAsCallable($key)) { + return $key; + } + if (func_num_args() === 1) { $value = true; @@ -866,6 +1043,7 @@ protected function operatorForWhere($key, $operator = null, $value = null) case '>=': return $retrieved >= $value; case '===': return $retrieved === $value; case '!==': return $retrieved !== $value; + case '<=>': return $retrieved <=> $value; } }; } @@ -893,8 +1071,38 @@ protected function valueRetriever($value) return $value; } - return function ($item) use ($value) { - return data_get($item, $value); - }; + return fn ($item) => data_get($item, $value); + } + + /** + * Make a function to check an item's equality. + * + * @param mixed $value + * @return \Closure(mixed): bool + */ + protected function equality($value) + { + return fn ($item) => $item === $value; + } + + /** + * Make a function using another function, by negating its result. + * + * @param \Closure $callback + * @return \Closure + */ + protected function negate(Closure $callback) + { + return fn (...$params) => ! $callback(...$params); + } + + /** + * Make a function that returns what's passed to it. + * + * @return \Closure(TValue): TValue + */ + protected function identity() + { + return fn ($value) => $value; } } diff --git a/src/Support/Traits/Macroable.php b/src/Support/Traits/Macroable.php index e712327..fb65dfc 100644 --- a/src/Support/Traits/Macroable.php +++ b/src/Support/Traits/Macroable.php @@ -1,4 +1,5 @@ bindTo(null, static::class); } - return call_user_func_array(static::$macros[$method], $parameters); + return $macro(...$parameters); } /** @@ -107,9 +119,9 @@ public function __call($method, $parameters) $macro = static::$macros[$method]; if ($macro instanceof Closure) { - return call_user_func_array($macro->bindTo($this, static::class), $parameters); + $macro = $macro->bindTo($this, static::class); } - return call_user_func_array($macro, $parameters); + return $macro(...$parameters); } } diff --git a/src/Support/helpers.php b/src/Support/helpers.php index 37e8bc8..6d6e539 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -1,4 +1,5 @@ toHtml(); } + if (!is_string($value)) { + $value = (string) $value; + } + return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', $doubleEncode); } -// } +} -if (! function_exists('e')) { +if (! function_exists('value')) { /** * Return the default value of the given value. * @@ -174,18 +190,6 @@ function data_get($target, $key, $default = null) } } -// if (! function_exists('collect')) { -/** - * Create a collection from the given value. - * - * @param mixed $value - * @return Collection - */ - function collect($value = null) - { - return new Collection($value); - } -// } if (! function_exists('windows_os')) { /** @@ -227,3 +231,14 @@ function hash_fit($data, $binary = false, $options = []) return hash('xxh128', $data, $binary, $options); } } + +if (!function_exists('uuid')) { + function uuid() + { + $chars = md5(uniqid((string) mt_rand(), true)); + $uuid = substr($chars, 0, 8) . '-' . substr($chars, 8, 4) . '-' . substr($chars, 12, 4) . '-' + . substr($chars, 16, 4) . '-' + . substr($chars, 20, 12); + return $uuid; + } +} \ No newline at end of file diff --git a/src/View/AnonymousComponent.php b/src/View/AnonymousComponent.php index eba6436..77dc5ea 100644 --- a/src/View/AnonymousComponent.php +++ b/src/View/AnonymousComponent.php @@ -1,4 +1,5 @@ attributes = $this->attributes ?: $this->newAttributeBag(); - return array_merge( ($this->data['attributes'] ?? null)?->getAttributes() ?: [], $this->attributes->getAttributes(), diff --git a/src/View/AppendableAttributeValue.php b/src/View/AppendableAttributeValue.php index f275801..b5983e9 100644 --- a/src/View/AppendableAttributeValue.php +++ b/src/View/AppendableAttributeValue.php @@ -1,4 +1,5 @@ cachePath)) { - $contents = $this->compileString($this->files->get($this->getPath())); + $contents = $this->compileString($this->getViewContent($this->getPath())); if (! empty($this->getPath())) { $contents = $this->appendFilePath($contents); @@ -179,7 +179,7 @@ public function compile($path = null) $compiledPath = $this->getCompiledPath($this->getPath()) ); - $this->files->put($compiledPath, $contents); + $this->putViewContent($compiledPath, $contents); } } @@ -443,6 +443,10 @@ protected function restoreRawContent($result) */ protected function getRawPlaceholder($replace) { + if (is_numeric($replace)) { + $replace = (string) $replace; + } + return str_replace('#', $replace, '@__raw_block_#__@'); } diff --git a/src/View/Compilers/Compiler.php b/src/View/Compilers/Compiler.php index 24a6c26..db31b88 100644 --- a/src/View/Compilers/Compiler.php +++ b/src/View/Compilers/Compiler.php @@ -1,10 +1,11 @@ files = $files; - - $config = app('config')->get('view'); - - /* - if (! $cachePath) { - throw new InvalidArgumentException('Please provide a valid cache path.'); - }*/ - - if (empty($config['compiled'])) { - $config['compiled'] = app()->getRuntimePath(); // . 'view' . DS - } - - $this->cachePath = $config['compiled']; - $this->isCache = $config['cache'] ?? false; - $this->compiledExtension = $config['compiled_extension'] ?? 'php'; - - $theme = $config['theme'] ?? ''; - - // 设置到view文件夹 - $this->cachePath = $this->cachePath .'view'. DS; - - // 如果有主题, 则增加主题文件夹 - if ($theme) { - $this->cachePath = $this->cachePath . $theme . DS; - } + $this->cachePath = $cachePath; + $this->isCache = $shouldCache; + $this->compiledExtension = $compiledExtension; } /** @@ -84,7 +61,6 @@ public function __construct(Filesystem $files/*, $cachePath, $isCache = true, $c */ public function getCompiledPath($path) { - // preg_match("/;app;([a-zA-Z]+);view;([a-zA-Z]+);/", str_replace('\\', ';', $path), $appendPaths); // return $this->cachePath . '/' . sha1($path) . '.' . basename($path) .'.'. $this->compiledExtension; return $this->cachePath .'/'. hash_fit('v2'. basename($path)) .'.'. $this->compiledExtension; } @@ -102,7 +78,7 @@ public function isExpired($path) // If the compiled file doesn't exist we will indicate that the view is expired // so that it can be re-compiled. Else, we will verify the last modification // of the views is less than the modification times of the compiled views. - if (! $this->files->exists($compiled)) { + if (! file_exists($compiled)) { return true; } @@ -110,8 +86,7 @@ public function isExpired($path) return true; } - return $this->files->lastModified($path) >= - $this->files->lastModified($compiled); + return filemtime($path) >= filemtime($compiled); } /** @@ -122,8 +97,65 @@ public function isExpired($path) */ protected function ensureCompiledDirectoryExists($path) { - if (! $this->files->exists(dirname($path))) { - $this->files->makeDirectory(dirname($path), 0777, true, true); + if (! file_exists(dirname($path))) { + @mkdir(dirname($path), 0777, true); + } + } + + /** + * Write the contents of a file. + * + * @param string $path + * @param string $contents + * @param bool $lock + * @return int|bool + */ + public function putViewContent($path, $contents, $lock = false) + { + return file_put_contents($path, $contents, $lock ? LOCK_EX : 0); + } + + /** + * Get the contents of a file. + * + * @param string $path + * @return string + */ + public function getViewContent($path, $lock = false) + { + if (is_file($path)) { + return $lock ? $this->sharedGet($path) : file_get_contents($path); } + + throw new ViewException("View file does not exist at path {$path}"); + } + + /** + * Get contents of a file with shared access. + * + * @param string $path + * @return string + */ + public function sharedGet($path) + { + $contents = ''; + + $handle = fopen($path, 'rb'); + + if ($handle) { + try { + if (flock($handle, LOCK_SH)) { + clearstatcache(true, $path); + + $contents = fread($handle, $this->size($path) ?: 1); + + flock($handle, LOCK_UN); + } + } finally { + fclose($handle); + } + } + + return $contents; } } diff --git a/src/View/Compilers/CompilerInterface.php b/src/View/Compilers/CompilerInterface.php index dfcb023..cb13abf 100644 --- a/src/View/Compilers/CompilerInterface.php +++ b/src/View/Compilers/CompilerInterface.php @@ -1,4 +1,5 @@ * @author Taylor Otwell @@ -61,8 +60,7 @@ public function __construct(array $aliases = [], array $namespaces = [], ?BladeC { $this->aliases = $aliases; $this->namespaces = $namespaces; - - $this->blade = $blade ?: new BladeCompiler(new Filesystem, sys_get_temp_dir()); + $this->blade = $blade ?: new BladeCompiler(sys_get_temp_dir()); } /** @@ -264,8 +262,6 @@ public function componentClass(string $component) { $viewFactory = Container::getInstance()->make('blade.view'); - // dd($this->aliases, $component); - if (isset($this->aliases[$component])) { if (class_exists($alias = $this->aliases[$component])) { return $alias; @@ -414,10 +410,7 @@ public function findClassByComponent(string $component) */ public function guessClassName(string $component) { - /*$namespace = Container::getInstance() - ->make(Application::class) - ->getNamespace();*/ - $namespace = 'index\\'; + $namespace = Container::getInstance()->getNamespace() ?: 'app\\'; $class = $this->formatClassName($component); @@ -452,7 +445,7 @@ public function guessViewName($name, $prefix = 'components.') $prefix .= '.'; } - $delimiter = \Illuminate\View\ViewFinderInterface::HINT_PATH_DELIMITER; + $delimiter = ViewName::HINT_PATH_DELIMITER; if (str_contains($name, $delimiter)) { return Str::replaceFirst($delimiter, $delimiter.$prefix, $name); diff --git a/src/View/Compilers/Concerns/CompilesAuthorizations.php b/src/View/Compilers/Concerns/CompilesAuthorizations.php index 35496df..f21aa81 100644 --- a/src/View/Compilers/Concerns/CompilesAuthorizations.php +++ b/src/View/Compilers/Concerns/CompilesAuthorizations.php @@ -1,4 +1,5 @@ compileEchoDefaults($matches[2])}; ?>{$whitespace}"; + return $matches[1] + ? substr($matches[0], 1) + : "wrapInEchoHandler($matches[2])}; ?>{$whitespace}"; }; return preg_replace_callback($pattern, $callback, $value); @@ -91,8 +100,7 @@ protected function compileRegularEchos($value) $callback = function ($matches) { $whitespace = empty($matches[3]) ? '' : $matches[3].$matches[3]; - // $wrapped = sprintf($this->echoFormat, $matches[2]); - $wrapped = sprintf($this->echoFormat, $this->compileEchoDefaults($matches[2])); + $wrapped = sprintf($this->echoFormat, $this->wrapInEchoHandler($matches[2])); return $matches[1] ? substr($matches[0], 1) : "{$whitespace}"; }; @@ -115,22 +123,12 @@ protected function compileEscapedEchos($value) return $matches[1] ? $matches[0] - : "compileEchoDefaults($matches[2])}); ?>{$whitespace}"; + : "wrapInEchoHandler($matches[2])}); ?>{$whitespace}"; }; return preg_replace_callback($pattern, $callback, $value); } - /** - * Compile the default values for the echo statement. - * - * @param string $value - * @return string - */ - public function compileEchoDefaults($value) { - return preg_replace('/^(?=\$)(.+?)(?:\s+and\s+)(.+?)$/s', 'empty($1) ? $2 : $1', $value); - } - /** * Add an instance of the blade echo handler to the start of the compiled string. * @@ -158,7 +156,7 @@ protected function wrapInEchoHandler($value) return empty($this->echoHandlers) ? $value : '$__bladeCompiler->applyEchoHandler('.$value.')'; } - + /** * Apply the echo handler for the value if it exists. * @@ -173,4 +171,100 @@ public function applyEchoHandler($value) return $value; } + + /** + * Compile the default values for the echo statement. + * + * @param string $value + * @return string + */ + public function compileEchoDefaults($value) { + return preg_replace('/^(?=\$)(.+?)(?:\s+and\s+)(.+?)$/s', 'empty($1) ? $2 : $1', $value); + } + + /** + * Get the class name of the first parameter of the given Closure. + * + * @param \Closure $closure + * @return string + * + * @throws \ReflectionException + * @throws \RuntimeException + */ + protected function firstClosureParameterType(Closure $closure) + { + $types = array_values($this->closureParameterTypes($closure)); + + if (! $types) { + throw new RuntimeException('The given Closure has no parameters.'); + } + + if ($types[0] === null) { + throw new RuntimeException('The first parameter of the given Closure is missing a type hint.'); + } + + return $types[0]; + } + + /** + * Get the class names / types of the parameters of the given Closure. + * + * @param \Closure $closure + * @return array + * + * @throws \ReflectionException + */ + protected function closureParameterTypes(Closure $closure) + { + $reflection = new ReflectionFunction($closure); + + return collect($reflection->getParameters())->mapWithKeys(function ($parameter) { + if ($parameter->isVariadic()) { + return [$parameter->getName() => null]; + } + + return [$parameter->getName() => self::getParameterClassName($parameter)]; + })->all(); + } + + /** + * Get the class name of the given parameter's type, if possible. + * + * @param \ReflectionParameter $parameter + * @return string|null + */ + public static function getParameterClassName($parameter) + { + $type = $parameter->getType(); + + if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { + return; + } + + return static::getTypeName($parameter, $type); + } + + /** + * Get the given type's class name. + * + * @param \ReflectionParameter $parameter + * @param \ReflectionNamedType $type + * @return string + */ + protected static function getTypeName($parameter, $type) + { + $name = $type->getName(); + + if (! is_null($class = $parameter->getDeclaringClass())) { + if ($name === 'self') { + return $class->getName(); + } + + if ($name === 'parent' && $parent = $class->getParentClass()) { + return $parent->getName(); + } + } + + return $name; + } } diff --git a/src/View/Compilers/Concerns/CompilesErrors.php b/src/View/Compilers/Concerns/CompilesErrors.php deleted file mode 100644 index 252871d..0000000 --- a/src/View/Compilers/Concerns/CompilesErrors.php +++ /dev/null @@ -1,34 +0,0 @@ -stripParentheses($expression); - - return 'has('.$expression.')) : -if (isset($message)) { $messageCache = $message; } -$message = $errors->first('.$expression.'); ?>'; - } - - /** - * Compile the enderror statements into valid PHP. - * - * @param string $expression - * @return string - */ - protected function compileEnderror($expression) - { - return ''; - } -} diff --git a/src/View/Compilers/Concerns/CompilesFragments.php b/src/View/Compilers/Concerns/CompilesFragments.php index 607b6dd..c3c96a8 100644 --- a/src/View/Compilers/Concerns/CompilesFragments.php +++ b/src/View/Compilers/Concerns/CompilesFragments.php @@ -1,4 +1,5 @@ toHtml() ?>", - Js::class, $this->stripParentheses($expression) - ); - } -} diff --git a/src/View/Compilers/Concerns/CompilesLayouts.php b/src/View/Compilers/Concerns/CompilesLayouts.php index aaef617..34d2207 100644 --- a/src/View/Compilers/Concerns/CompilesLayouts.php +++ b/src/View/Compilers/Concerns/CompilesLayouts.php @@ -1,4 +1,5 @@ hasRenderedOnce('.$id.')): $__env->markAsRenderedOnce('.$id.'); $__env->startPush('.$stack.'); ?>'; @@ -89,7 +90,7 @@ protected function compilePrependOnce($expression) [$stack, $id] = [$parts[0], $parts[1] ?? '']; - $id = trim($id) ?: "'".(string) Str::uuid()."'"; + $id = trim($id) ?: "'".(string) uuid()."'"; return 'hasRenderedOnce('.$id.')): $__env->markAsRenderedOnce('.$id.'); $__env->startPrepend('.$stack.'); ?>'; diff --git a/src/View/Compilers/Concerns/CompilesStyles.php b/src/View/Compilers/Concerns/CompilesStyles.php index 6c71506..f57569f 100644 --- a/src/View/Compilers/Concerns/CompilesStyles.php +++ b/src/View/Compilers/Concerns/CompilesStyles.php @@ -1,4 +1,5 @@ componentData[count($this->componentStack)], ['slot' => $defaultSlot], $this->slots[count($this->componentStack)], - ['__laravel_slots' => $slots] + ['__thinkphp_slots' => $slots] ); } diff --git a/src/View/Concerns/ManagesFragments.php b/src/View/Concerns/ManagesFragments.php index 7273da6..a243516 100644 --- a/src/View/Concerns/ManagesFragments.php +++ b/src/View/Concerns/ManagesFragments.php @@ -1,4 +1,5 @@ compileProps($bindings), $this->compileBindings($bindings), class_exists($class) ? '{{ $attributes }}' : '', - $this->compileSlots($data['__laravel_slots']), + $this->compileSlots($data['__thinkphp_slots']), '{{ $slot ?? "" }}', ], $template @@ -162,10 +163,11 @@ protected function bindings(string $class) protected function compiler() { if (! static::$compiler) { + $blade = Container::getInstance()->make('blade.compiler'); static::$compiler = new ComponentTagCompiler( - Container::getInstance()->make('blade.compiler')->getClassComponentAliases(), - Container::getInstance()->make('blade.compiler')->getClassComponentNamespaces(), - Container::getInstance()->make('blade.compiler') + $blade->getClassComponentAliases(), + $blade->getClassComponentNamespaces(), + $blade ); } diff --git a/src/View/Engines/CompilerEngine.php b/src/View/Engines/CompilerEngine.php index 93f54c7..0f7e9e6 100644 --- a/src/View/Engines/CompilerEngine.php +++ b/src/View/Engines/CompilerEngine.php @@ -5,7 +5,6 @@ use ErrorException; use Exception; use Throwable; -use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\View\Compilers\CompilerInterface; use Illuminate\View\ViewException; @@ -14,7 +13,6 @@ use function Illuminate\Support\last; use function Illuminate\Support\str; - class CompilerEngine extends PhpEngine { /** @@ -42,12 +40,11 @@ class CompilerEngine extends PhpEngine * Create a new compiler engine instance. * * @param \Illuminate\View\Compilers\CompilerInterface $compiler - * @param \Illuminate\Filesystem\Filesystem|null $files * @return void */ - public function __construct(CompilerInterface $compiler, Filesystem $files = null) + public function __construct(CompilerInterface $compiler) { - parent::__construct($files ?: new Filesystem); + parent::__construct(); $this->compiler = $compiler; } diff --git a/src/View/Engines/PhpEngine.php b/src/View/Engines/PhpEngine.php index 13525ae..d3061d6 100644 --- a/src/View/Engines/PhpEngine.php +++ b/src/View/Engines/PhpEngine.php @@ -3,28 +3,18 @@ namespace Illuminate\View\Engines; use Illuminate\Contracts\View\Engine; -use Illuminate\Filesystem\Filesystem; +use Illuminate\View\ViewException; use Throwable; class PhpEngine implements Engine { - /** - * The filesystem instance. - * - * @var \Illuminate\Filesystem\Filesystem - */ - protected $files; - /** * Create a new file engine instance. * - * @param \Illuminate\Filesystem\Filesystem $files * @return void */ - public function __construct(Filesystem $files) - { - $this->files = $files; - } + public function __construct() + {} /** * Get the evaluated contents of the view. @@ -55,7 +45,7 @@ protected function evaluatePath($path, $data) // flush out any stray output that might get out before an error occurs or // an exception is thrown. This prevents any partial views from leaking. try { - $this->files->getRequire($path, $data); + $this->getRequire($path, $data); } catch (Throwable $e) { $this->handleViewException($e, $obLevel); } @@ -80,4 +70,29 @@ protected function handleViewException(Throwable $e, $obLevel) throw $e; } + + /** + * Get the returned value of a file. + * + * @param string $path + * @param array $data + * @return mixed + * + * @throws \Illuminate\View\ViewException + */ + public function getRequire($path, array $data = []) + { + if (is_file($path)) { + $__path = $path; + $__data = $data; + + return (static function () use ($__path, $__data) { + extract($__data, EXTR_SKIP); + + return require $__path; + })(); + } + + throw new ViewException("View file does not exist at path {$path}."); + } } diff --git a/src/View/Factory.php b/src/View/Factory.php index c274bc1..c7942f4 100644 --- a/src/View/Factory.php +++ b/src/View/Factory.php @@ -1,4 +1,5 @@ 'view', // 模版主题 'theme' => '', - // 模板起始路径 - 'base_path' => '', - // 模板文件后缀 - 'suffix' => 'blade.php', - // 模板文件名分隔符 - 'depr' => DS, // 缓存路径 'compiled' => '', + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法 + 'auto_rule' => 1, + // 视图目录名 + 'view_dir_name' => 'view', + // 模板起始路径 + 'view_path' => '', + // 模板后缀 + 'view_suffix' => 'html.php', + // 模板文件名分隔符 + 'view_depr' => DS, // 是否开启模板编译缓存,设为false则每次都会重新编译 - 'cache' => true, + 'tpl_cache' => true, ]; /** @@ -78,7 +82,6 @@ class Factory implements FactoryContract * @var array */ protected $extensions = [ - 'html.php' => 'blade', 'blade.php' => 'blade', 'php' => 'php', 'css' => 'file', @@ -111,78 +114,21 @@ class Factory implements FactoryContract /** * Create a new view factory instance. * + * @param \think\App $app * @param \Illuminate\View\Engines\EngineResolver $engines * @param \Illuminate\View\ViewFinderInterface $finder * @return void */ - public function __construct(EngineResolver $engines, $finder, App $app) + public function __construct(App $app, EngineResolver $engines/*, FileViewFinder $finder*/) { $this->app = $app; - $this->finder = $finder; + // $this->finder = $finder; $this->engines = $engines; - - $config = $this->app->get('config')->get('view'); - - $this->config = array_merge($this->config, $config); - - if (empty($this->config['compiled'])) { - $this->config['compiled'] = $app->getRuntimePath(); - } - - // 缓存主题路径 - if (!empty($this->config['theme'])) { - $this->config['compiled'] .= $this->config['theme'] . DS; - } - - // default view path - $cutrrentApp = $this->app->http->getName(); - - if ($this->config['base_path']) { - $path = $this->config['base_path']; - } else { - $appName = $cutrrentApp; - $view = $this->config['dir_name']; - - if (is_dir($this->app->getAppPath() . $view)) { - $path = $this->app->getAppPath() . $view . DS; - } else { - $path = $this->app->getRootPath() . $view . DS . ($appName ? $appName . DS : ''); - } - } - - // 设置主题路径 - if (!empty($this->config['theme'])) { - // default 主题备用 - $path .= $this->config['theme'] . DS; - } - - $this->app->get('view.finder')->addLocation($path); - - // $finder->addLocation($path); // . 'components'. DS - - // debug 不缓存 - if ($this->app->isDebug()) { - // $this->config['cache'] = false; - } + $this->config = array_merge($this->config, $this->app->get('config')->get('view', [])); $this->share('__env', $this); } - /** - * 设置模板主题 - * - * @param string $path 模板文件路径 - * @return bool - */ - public function theme($path = '') - { - if (empty($this->config['theme'])) { - return $path; - } - - return $path .= $this->config['theme'] . DS; - } - /** * 根据模版获取实际路径 * @@ -193,7 +139,7 @@ public function findView($template = '') { $templatePath = ''; - $template = $this->viewName($template); + $template = ViewName::normalize2tp($template); if ('' == pathinfo($template, PATHINFO_EXTENSION)) { $templatePath = $this->parseTemplate($template); @@ -201,7 +147,7 @@ public function findView($template = '') // 模板不存在 抛出异常 if (!$templatePath || !is_file($templatePath)) { - throw new ViewNotFoundException('View not exists:' . $this->viewName($template, true), $templatePath); + throw new ViewNotFoundException('View not exists:' . ViewName::normalize2tp($template, true), $templatePath); } return $templatePath; @@ -216,7 +162,7 @@ public function findView($template = '') */ public function exists(string $view): bool { - $view = $this->viewName($view); + $view = ViewName::normalize2tp($view); if ('' == pathinfo($view, PATHINFO_EXTENSION)) { $view = $this->parseTemplate($view); @@ -235,28 +181,29 @@ public function exists(string $view): bool */ public function make($view, $data = [], $mergeData = []) { + $path = null; if (is_file($view)) { $path = $view; } else { - $path = ''; - // $view = ViewName::normalize2($view); + $_view = ViewName::normalize2tp($view); - $view = $this->viewName($view); - if ('' == pathinfo($view, PATHINFO_EXTENSION)) { - $path = $this->parseTemplate($view); + // thinkphp方式查找模版 + if ('' == pathinfo($_view, PATHINFO_EXTENSION)) { + $path = $this->parseTemplate($_view); } - if (!$path || !is_file($path)) { + // blade方式查找模版 + /*if (!is_file($path)) { $path = $this->finder->find( $this->normalizeName($view) ); - } + }*/ } // 模板不存在 抛出异常 - if (!$path || !is_file($path)) { - throw new ViewNotFoundException('View not exists:' . $this->viewName($view, true), $path); + if (!is_file($path)) { + throw new ViewNotFoundException('View not exists:' . ViewName::normalize2tp($view, true), $path); } // 记录视图信息 @@ -268,7 +215,7 @@ public function make($view, $data = [], $mergeData = []) // the caller for rendering or performing other view manipulations on this. $data = array_merge($mergeData, $this->parseData($data)); - return tap($this->viewInstance($path, $path, $data), function ($view) { + return tap($this->viewInstance($view, $path, $data), function ($view) { $this->callCreator($view); }); } @@ -307,38 +254,43 @@ public function fetch(string $view, array $data = [], array $mergeData = []) * 获取模版所在基础路径 * * @param string $template 模板文件规则 - * @return string + * @return array */ - private function parseBasePath(string $template): string + private function parseBasePath(string $template): array { + $request = $this->app->request; + + $app = null; + $depr = $this->config['view_depr']; + $view = $this->config['view_dir_name'] ?: 'view'; + // 获取视图根目录 if (strpos($template, '@')) { // 跨应用调用 [$app, $template] = explode('@', $template); } - $cutrrentApp = $this->app->http->getName(); + // 多应用模式 + if(class_exists('\think\app\MultiApp')) { - if ($this->config['base_path'] && !isset($app)) { - $path = $this->config['base_path']; - } else { - $appName = isset($app) ? $app : $cutrrentApp; - $view = $this->config['dir_name']; + $appName = is_null($app) ? $this->app->http->getName() : $app; if (is_dir($this->app->getAppPath() . $view)) { - $path = isset($app) ? $this->app->getBasePath() . ($appName ? $appName . DS : '') . $view . DS : $this->app->getAppPath() . $view . DS; + $path = (is_null($app) ? $this->app->getAppPath() : $this->app->getBasePath() . $appName) . $depr . $view . $depr; } else { - $path = $this->app->getRootPath() . $view . DS . ($appName ? $appName . DS : ''); + $path = $this->app->getRootPath() . $view . $depr . $appName . $depr; } + } else { + // 单应用模式 + $path = $this->app->getRootPath() . $view . $depr; } // 设置主题路径 if (!empty($this->config['theme'])) { - // default 主题备用 - $path .= $this->config['theme'] . DS; + $path .= $this->config['theme'] . $depr; } - return $path; + return [$path, $template]; } /** @@ -349,9 +301,10 @@ private function parseBasePath(string $template): string */ private function parseTemplate(string $template): string { - $depr = $this->config['depr']; $request = $this->app->request; - $path = $this->parseBasePath($template); + $depr = $this->config['view_depr']; + + [$path, $template] = $this->parseBasePath($template); if (0 !== strpos($template, '/')) { $template = str_replace(['/', ':'], $depr, $template); @@ -374,16 +327,16 @@ private function parseTemplate(string $template): string $template = Str::snake($request->action()); } - $template = str_replace('.', DS, $controller) . $depr . $template; + $template = str_replace('.', $depr, $controller) . $depr . $template; } elseif (false === strpos($template, $depr)) { - $template = str_replace('.', DS, $controller) . $depr . $template; + $template = str_replace('.', $depr, $controller) . $depr . $template; } } } else { $template = str_replace(['/', ':'], $depr, substr($template, 1)); } - $template = $path . ltrim($template, '/') . '.' . ltrim($this->config['suffix'], '.'); + $template = $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); if (is_file($template)) { return $template; @@ -391,18 +344,25 @@ private function parseTemplate(string $template): string // 未设置主题, 尝试先去default查找 if(empty($this->config['theme'])) { - - $default = str_replace(DS .'view'. DS, DS .'view'. DS .'default'. DS, $template); + $default = str_replace( + $depr .'view'. $depr, + $depr .'view'. $depr .'default'. $depr, + $template + ); if (is_file($default)) { return $default; } } - // 默认主题不存在模版, 降级删除default主题继续查找 - if (strpos($template, DS .'view'. DS . 'default' . DS) !== false) { - $default = str_replace(DS .'view'. DS . 'default' . DS, DS .'view'. DS, $template); + // 默认主题不存在模版, 降级删除default主题继续查找 + if (strpos($template, $depr .'view'. $depr . 'default' . $depr) !== false) { + $default = str_replace( + $depr .'view'. $depr .'default'. $depr, + $depr .'view'. $depr, + $template + ); if (is_file($default)) { return $default; @@ -410,11 +370,11 @@ private function parseTemplate(string $template): string } // 已设置主题, 但是找不到模版, 尝试降级为default主题 - if (strpos($template, DS .'view'. DS . $this->config['theme'] . DS) !== false) { - + if (strpos($template, $depr .'view'. $depr . $this->config['theme'] . $depr) !== false) { $default = str_replace( - DS . $this->config['theme'] . DS, - DS . 'default' . DS, $template + $depr . $this->config['theme'] . $depr, + $depr .'default'. $depr, + $template ); if (is_file($default)) { @@ -425,26 +385,6 @@ private function parseTemplate(string $template): string return ''; } - /** - * Normalize the given template. - * - * @param string $name - * @return string - */ - private function viewName($template = '', $isRaw = false) - { - - if($isRaw && strpos($template, '/')) { - return str_replace('/', '.', $template); - } - - if (strpos($template, '.')) { - $template = str_replace('.', '/', $template); - } - - return $template; - } - /** * Get the evaluated view contents for the given view. * @@ -876,4 +816,9 @@ public function callComposer($view) public function callCreator($view) { } + + public function __call($method, $params) + { + return call_user_func_array([$this->app->get('blade.compiler'), $method], $params); + } } diff --git a/src/View/FileViewFinder.php b/src/View/FileViewFinder.php index d9f5577..94b21ac 100644 --- a/src/View/FileViewFinder.php +++ b/src/View/FileViewFinder.php @@ -1,19 +1,12 @@ files = $files; $this->paths = array_map([$this, 'resolvePath'], $paths); if (isset($extensions)) { @@ -102,7 +93,7 @@ protected function findNamespacedView($name) */ protected function parseNamespaceSegments($name) { - $segments = explode(static::HINT_PATH_DELIMITER, $name); + $segments = explode(ViewName::HINT_PATH_DELIMITER, $name); if (count($segments) !== 2) { throw new InvalidArgumentException("View [{$name}] has an invalid name."); } @@ -125,7 +116,7 @@ protected function parseNamespaceSegments($name) */ protected function findInPaths($name, $paths) { - if (str_contains($name, '@')) { + /*if (str_contains($name, '@')) { [$app, $name] = explode('@', $name); $currentApp = app('http')->getName(); @@ -133,11 +124,11 @@ protected function findInPaths($name, $paths) $paths[] = base_path($app) .'view'; $this->addLocation(base_path($app) .'view'); } - } + }*/ foreach ((array) $paths as $path) { foreach ($this->getPossibleViewFiles($name) as $file) { - if ($this->files->exists($viewPath = $path.'/'.$file)) { + if (file_exists($viewPath = $path.'/'.$file)) { return $viewPath; } } @@ -261,7 +252,7 @@ public function addExtension($extension) */ public function hasHintInformation($name) { - return strpos($name, static::HINT_PATH_DELIMITER) > 0; + return strpos($name, ViewName::HINT_PATH_DELIMITER) > 0; } /** @@ -274,16 +265,6 @@ public function flush() $this->views = []; } - /** - * Get the filesystem instance. - * - * @return \Illuminate\Filesystem\Filesystem - */ - public function getFilesystem() - { - return $this->files; - } - /** * Set the active view paths. * diff --git a/src/View/View.php b/src/View/View.php index fe94c78..18b0959 100644 --- a/src/View/View.php +++ b/src/View/View.php @@ -1,10 +1,10 @@ render(); } - + /** * Get the string contents of the view. * @@ -167,7 +168,7 @@ public function render(callable $callback = null) */ protected function renderContents() { - // We will keep track of the amount of views being rendered so we can flush + // We will keep track of the number of views being rendered so we can flush // the section after the complete rendering operation is done. This will // clear out the sections for any separate views that may be rendered. $this->factory->incrementRender(); @@ -177,7 +178,7 @@ protected function renderContents() $contents = $this->getContents(); // Once we've finished rendering the view, we'll decrement the render count - // so that each sections get flushed out next time a view is created and + // so that each section gets flushed out next time a view is created and // no old sections are staying around in the memory of an environment. $this->factory->decrementRender(); @@ -230,7 +231,7 @@ public function renderSections() * Add a piece of data to the view. * * @param string|array $key - * @param mixed $value + * @param mixed $value * @return $this */ public function with($key, $value = null) @@ -249,7 +250,7 @@ public function with($key, $value = null) * * @param string $key * @param string $view - * @param array $data + * @param array $data * @return $this */ public function nest($key, $view, array $data = []) @@ -354,7 +355,7 @@ public function offsetGet($key): mixed * Set a piece of data on the view. * * @param string $key - * @param mixed $value + * @param mixed $value * @return void */ public function offsetSet($key, $value): void @@ -388,7 +389,7 @@ public function &__get($key) * Set a piece of data on the view. * * @param string $key - * @param mixed $value + * @param mixed $value * @return void */ public function __set($key, $value) @@ -422,7 +423,7 @@ public function __unset($key) * Dynamically bind parameters to the view. * * @param string $method - * @param array $parameters + * @param array $parameters * @return \Illuminate\View\View * * @throws \BadMethodCallException diff --git a/src/View/ViewException.php b/src/View/ViewException.php index 1c9b7dc..693ab1a 100644 --- a/src/View/ViewException.php +++ b/src/View/ViewException.php @@ -1,4 +1,5 @@ getName(); + // if ($namespace != $app) {} + + return $namespace . $delimiter . str_replace('/', '.', $name); } /** * Normalize the given template. * * @param string $name + * @param bool $raw * @return string */ - public static function normalize2($template = '', $isRaw = false) + public static function normalize2tp($template = '', $raw = false) { - if($isRaw && strpos($template, '/')) { + if($raw && strpos($template, '/')) { return str_replace('/', '.', $template); } diff --git a/src/facade/View.php b/src/facade/View.php index 8766dd9..e062655 100644 --- a/src/facade/View.php +++ b/src/facade/View.php @@ -10,7 +10,6 @@ class View extends Facade { protected static function getFacadeClass() { - // return 'blade.compiler'; return 'blade.view'; } } \ No newline at end of file diff --git a/src/helper.php b/src/helper.php index ef89406..a6d1b12 100644 --- a/src/helper.php +++ b/src/helper.php @@ -1,35 +1,10 @@ make($view, $data, $mergeData); - } -} - -if (! function_exists('blade')) { +if (! function_exists('view')) { /** * Get the evaluated view contents for the given view. * @@ -38,7 +13,7 @@ function v($view = null, $data = [], $mergeData = []) * @param array $mergeData * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory */ - function blade($view = null, $data = [], $mergeData = []) + function view($view = null, $data = [], $mergeData = []) { $factory = app('blade.view'); @@ -58,7 +33,7 @@ function blade($view = null, $data = [], $mergeData = []) */ function csrf_field(string $name = '__token__', string $type = 'md5'): string { - return ''; + return ''; } }