├── README-ZH.md ├── README.md ├── composer.json ├── logo.svg └── src ├── Collect ├── Conditionable │ └── HigherOrderWhenProxy.php ├── Contracts │ └── Support │ │ ├── Arrayable.php │ │ ├── CanBeEscapedWhenCastToString.php │ │ ├── Htmlable.php │ │ └── Jsonable.php └── Support │ ├── Arr.php │ ├── Collection.php │ ├── Enumerable.php │ ├── HigherOrderCollectionProxy.php │ ├── LazyCollection.php │ ├── Str.php │ ├── Traits │ ├── Conditionable.php │ ├── EnumeratesValues.php │ ├── Macroable.php │ └── Tappable.php │ ├── alias.php │ └── helpers.php ├── Config.php ├── Contracts ├── PluginContract.php └── ServiceProviderContract.php ├── Dom ├── Dom.php ├── Elements.php └── Query.php ├── Exceptions └── ServiceNotFoundException.php ├── Kernel.php ├── Providers ├── EncodeServiceProvider.php ├── HttpServiceProvider.php ├── PluginServiceProvider.php └── SystemServiceProvider.php ├── QueryList.php └── Services ├── EncodeService.php ├── HttpService.php ├── MultiRequestService.php └── PluginService.php /README-ZH.md: -------------------------------------------------------------------------------- 1 |

2 | QueryList 3 |
4 |
5 |

6 | 7 | # QueryList 简介 8 | `QueryList`是一套简洁、优雅、可扩展的PHP采集工具(爬虫),基于phpQuery。 9 | 10 | ## 特性 11 | - 拥有与jQuery完全相同的CSS3 DOM选择器 12 | - 拥有与jQuery完全相同的DOM操作API 13 | - 拥有通用的列表采集方案 14 | - 拥有强大的HTTP请求套件,轻松实现如:模拟登陆、伪造浏览器、HTTP代理等意复杂的网络请求 15 | - 拥有乱码解决方案 16 | - 拥有强大的内容过滤功能,可使用jQuey选择器来过滤内容 17 | - 拥有高度的模块化设计,扩展性强 18 | - 拥有富有表现力的API 19 | - 拥有高质量文档 20 | - 拥有丰富的插件 21 | - 拥有专业的问答社区和交流群 22 | 23 | 通过插件可以轻松实现诸如: 24 | - 多线程采集 25 | - 采集JavaScript动态渲染的页面 (PhantomJS/headless WebKit) 26 | - 图片本地化 27 | - 模拟浏览器行为,如:提交Form表单 28 | - 网络爬虫 29 | - ..... 30 | 31 | ## 环境要求 32 | - PHP >= 8.1 33 | 34 | > 如果你的PHP版本还停留在PHP5,或者不会使用Composer,你可以选择使用QueryList3,QueryList3支持php5.3以及手动安装。 35 | QueryList3 文档:http://v3.querylist.cc 36 | 37 | ## 安装 38 | 通过Composer安装: 39 | ``` 40 | composer require jaeger/querylist 41 | ``` 42 | 43 | ## 使用 44 | 45 | #### 元素操作 46 | - 采集「昵图网」所有图片地址 47 | 48 | ```php 49 | QueryList::get('http://www.nipic.com')->find('img')->attrs('src'); 50 | ``` 51 | - 采集百度搜索结果 52 | 53 | ```php 54 | $ql = QueryList::get('http://www.baidu.com/s?wd=QueryList'); 55 | 56 | $ql->find('title')->text(); // 获取网站标题 57 | $ql->find('meta[name=keywords]')->content; // 获取网站头部关键词 58 | 59 | $ql->find('h3>a')->texts(); //获取搜索结果标题列表 60 | $ql->find('h3>a')->attrs('href'); //获取搜索结果链接列表 61 | 62 | $ql->find('img')->src; //获取第一张图片的链接地址 63 | $ql->find('img:eq(1)')->src; //获取第二张图片的链接地址 64 | $ql->find('img')->eq(2)->src; //获取第三张图片的链接地址 65 | // 遍历所有图片 66 | $ql->find('img')->map(function($img){ 67 | echo $img->alt; //打印图片的alt属性 68 | }); 69 | ``` 70 | - 更多用法 71 | 72 | ```php 73 | $ql->find('#head')->append('
追加内容
')->find('div')->htmls(); 74 | $ql->find('.two')->children('img')->attrs('alt'); //获取class为two元素下的所有img孩子节点 75 | //遍历class为two元素下的所有孩子节点 76 | $data = $ql->find('.two')->children()->map(function ($item){ 77 | //用is判断节点类型 78 | if($item->is('a')){ 79 | return $item->text(); 80 | }elseif($item->is('img')) 81 | { 82 | return $item->alt; 83 | } 84 | }); 85 | 86 | $ql->find('a')->attr('href', 'newVal')->removeClass('className')->html('newHtml')->... 87 | $ql->find('div > p')->add('div > ul')->filter(':has(a)')->find('p:first')->nextAll()->andSelf()->... 88 | $ql->find('div.old')->replaceWith( $ql->find('div.new')->clone())->appendTo('.trash')->prepend('Deleted')->... 89 | ``` 90 | #### 列表采集 91 | 采集百度搜索结果列表的标题和链接: 92 | ```php 93 | $data = QueryList::get('http://www.baidu.com/s?wd=QueryList') 94 | // 设置采集规则 95 | ->rules([ 96 | 'title'=>array('h3','text'), 97 | 'link'=>array('h3>a','href') 98 | ]) 99 | ->query()->getData(); 100 | 101 | print_r($data->all()); 102 | ``` 103 | 采集结果: 104 | ``` 105 | Array 106 | ( 107 | [0] => Array 108 | ( 109 | [title] => QueryList|基于phpQuery的无比强大的PHP采集工具 110 | [link] => http://www.baidu.com/link?url=GU_YbDT2IHk4ns1tjG2I8_vjmH0SCJEAPuuZN 111 | ) 112 | [1] => Array 113 | ( 114 | [title] => PHP 用QueryList抓取网页内容 - wb145230 - 博客园 115 | [link] => http://www.baidu.com/link?url=zn0DXBnrvIF2ibRVW34KcRVFG1_bCdZvqvwIhUqiXaS 116 | ) 117 | [2] => Array 118 | ( 119 | [title] => 介绍- QueryList指导文档 120 | [link] => http://www.baidu.com/link?url=pSypvMovqS4v2sWeQo5fDBJ4EoYhXYi0Lxx 121 | ) 122 | //... 123 | ) 124 | ``` 125 | #### 编码转换 126 | ```php 127 | // 输出编码:UTF-8,输入编码:GB2312 128 | QueryList::get('https://top.etao.com')->encoding('UTF-8','GB2312')->find('a')->texts(); 129 | 130 | // 输出编码:UTF-8,输入编码:自动识别 131 | QueryList::get('https://top.etao.com')->encoding('UTF-8')->find('a')->texts(); 132 | ``` 133 | 134 | #### HTTP网络操作(GuzzleHttp) 135 | - 携带cookie登录新浪微博 136 | ```php 137 | //采集新浪微博需要登录才能访问的页面 138 | $ql = QueryList::get('http://weibo.com','param1=testvalue & params2=somevalue',[ 139 | 'headers' => [ 140 | //填写从浏览器获取到的cookie 141 | 'Cookie' => 'SINAGLOBAL=546064; wb_cmtLike_2112031=1; wvr=6;....' 142 | ] 143 | ]); 144 | //echo $ql->getHtml(); 145 | echo $ql->find('title')->text(); 146 | //输出: 我的首页 微博-随时随地发现新鲜事 147 | ``` 148 | - 使用Http代理 149 | ```php 150 | $urlParams = ['param1' => 'testvalue','params2' => 'somevalue']; 151 | $opts = [ 152 | // 设置http代理 153 | 'proxy' => 'http://222.141.11.17:8118', 154 | //设置超时时间,单位:秒 155 | 'timeout' => 30, 156 | // 伪造http头 157 | 'headers' => [ 158 | 'Referer' => 'https://querylist.cc/', 159 | 'User-Agent' => 'testing/1.0', 160 | 'Accept' => 'application/json', 161 | 'X-Foo' => ['Bar', 'Baz'], 162 | 'Cookie' => 'abc=111;xxx=222' 163 | ] 164 | ]; 165 | $ql->get('http://httpbin.org/get',$urlParams,$opts); 166 | // echo $ql->getHtml(); 167 | ``` 168 | 169 | - 模拟登录 170 | ```php 171 | // 用post登录 172 | $ql = QueryList::post('http://xxxx.com/login',[ 173 | 'username' => 'admin', 174 | 'password' => '123456' 175 | ])->get('http://xxx.com/admin'); 176 | //采集需要登录才能访问的页面 177 | $ql->get('http://xxx.com/admin/page'); 178 | //echo $ql->getHtml(); 179 | ``` 180 | 181 | #### Form表单操作 182 | 模拟登陆GitHub 183 | ```php 184 | // 获取QueryList实例 185 | $ql = QueryList::getInstance(); 186 | //获取到登录表单 187 | $form = $ql->get('https://github.com/login')->find('form'); 188 | 189 | //填写GitHub用户名和密码 190 | $form->find('input[name=login]')->val('your github username or email'); 191 | $form->find('input[name=password]')->val('your github password'); 192 | 193 | //序列化表单数据 194 | $fromData = $form->serializeArray(); 195 | $postData = []; 196 | foreach ($fromData as $item) { 197 | $postData[$item['name']] = $item['value']; 198 | } 199 | 200 | //提交登录表单 201 | $actionUrl = 'https://github.com'.$form->attr('action'); 202 | $ql->post($actionUrl,$postData); 203 | //判断登录是否成功 204 | // echo $ql->getHtml(); 205 | $userName = $ql->find('.header-nav-current-user>.css-truncate-target')->text(); 206 | if($userName) 207 | { 208 | echo '登录成功!欢迎你:'.$userName; 209 | }else{ 210 | echo '登录失败!'; 211 | } 212 | ``` 213 | #### Bind功能扩展 214 | 自定义扩展一个`myHttp`方法: 215 | ```php 216 | $ql = QueryList::getInstance(); 217 | 218 | //绑定一个myHttp方法到QueryList对象 219 | $ql->bind('myHttp',function ($url){ 220 | // $this 为当前的QueryList对象 221 | $html = file_get_contents($url); 222 | $this->setHtml($html); 223 | return $this; 224 | }); 225 | 226 | //然后就可以通过注册的名字来调用 227 | $data = $ql->myHttp('https://toutiao.io')->find('h3 a')->texts(); 228 | print_r($data->all()); 229 | ``` 230 | 或者把实现体封装到class,然后这样绑定: 231 | ```php 232 | $ql->bind('myHttp',function ($url){ 233 | return new MyHttp($this,$url); 234 | }); 235 | ``` 236 | 237 | #### 插件使用 238 | - 使用PhantomJS插件采集JavaScript动态渲染的页面: 239 | 240 | ```php 241 | // 安装时设置PhantomJS二进制文件路径 242 | $ql = QueryList::use(PhantomJs::class,'/usr/local/bin/phantomjs'); 243 | 244 | // 采集今日头条手机版 245 | $data = $ql->browser('https://m.toutiao.com')->find('p')->texts(); 246 | print_r($data->all()); 247 | 248 | // 使用HTTP代理 249 | $ql->browser('https://m.toutiao.com',false,[ 250 | '--proxy' => '192.168.1.42:8080', 251 | '--proxy-type' => 'http' 252 | ]) 253 | ``` 254 | 255 | - 使用CURL多线程插件,多线程采集GitHub排行榜: 256 | 257 | ```php 258 | $ql = QueryList::use(CurlMulti::class); 259 | $ql->curlMulti([ 260 | 'https://github.com/trending/php', 261 | 'https://github.com/trending/go', 262 | //.....more urls 263 | ]) 264 | // 每个任务成功完成调用此回调 265 | ->success(function (QueryList $ql,CurlMulti $curl,$r){ 266 | echo "Current url:{$r['info']['url']} \r\n"; 267 | $data = $ql->find('h3 a')->texts(); 268 | print_r($data->all()); 269 | }) 270 | // 每个任务失败回调 271 | ->error(function ($errorInfo,CurlMulti $curl){ 272 | echo "Current url:{$errorInfo['info']['url']} \r\n"; 273 | print_r($errorInfo['error']); 274 | }) 275 | ->start([ 276 | // 最大并发数 277 | 'maxThread' => 10, 278 | // 错误重试次数 279 | 'maxTry' => 3, 280 | ]); 281 | 282 | ``` 283 | 284 | ## 插件 285 | - [jae-jae/QueryList-PhantomJS](https://github.com/jae-jae/QueryList-PhantomJS): 使用PhantomJS采集JavaScript动态渲染的页面 286 | - [jae-jae/QueryList-CurlMulti](https://github.com/jae-jae/QueryList-CurlMulti) : Curl多线程采集 287 | - [jae-jae/QueryList-AbsoluteUrl](https://github.com/jae-jae/QueryList-AbsoluteUrl) : 转换URL相对路径到绝对路径 288 | - [jae-jae/QueryList-Rule-Google](https://github.com/jae-jae/QueryList-Rule-Google) : 谷歌搜索引擎 289 | - [jae-jae/QueryList-Rule-Baidu](https://github.com/jae-jae/QueryList-Rule-Baidu) : 百度搜索引擎 290 | 291 | 292 | 查看更多的QueryList插件和基于QueryList的产品:[QueryList社区力量](https://github.com/jae-jae/QueryList-Community) 293 | 294 | ## 贡献 295 | 欢迎为QueryList贡献代码。关于贡献插件可以查看:[QueryList插件贡献说明](https://github.com/jae-jae/QueryList-Community/blob/master/CONTRIBUTING.md) 296 | 297 | ## 寻求帮助? 298 | - QueryList主页: [http://querylist.cc](http://querylist.cc/) 299 | - QueryList文档: [http://doc.querylist.cc](http://doc.querylist.cc/) 300 | - QueryList问答:[http://wenda.querylist.cc](http://wenda.querylist.cc/) 301 | - QueryList交流QQ群:123266961 cafeEX 302 | - GitHub:https://github.com/jae-jae/QueryList 303 | - Git@OSC:http://git.oschina.net/jae/QueryList 304 | 305 | ## Author 306 | Jaeger 307 | 308 | ## Lisence 309 | QueryList is licensed under the license of MIT. See the LICENSE for more details. 310 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | QueryList 3 |
4 |
5 |

6 | 7 | # QueryList 8 | `QueryList` is a simple, elegant, extensible PHP Web Scraper (crawler/spider) ,based on phpQuery. 9 | 10 | [API Documentation](https://github.com/jae-jae/QueryList/wiki) 11 | 12 | [中文文档](README-ZH.md) 13 | 14 | ## Features 15 | - Have the same CSS3 DOM selector as jQuery 16 | - Have the same DOM manipulation API as jQuery 17 | - Have a generic list crawling program 18 | - Have a strong HTTP request suite, easy to achieve such as: simulated landing, forged browser, HTTP proxy and other complex network requests 19 | - Have a messy code solution 20 | - Have powerful content filtering, you can use the jQuey selector to filter content 21 | - Has a high degree of modular design, scalability and strong 22 | - Have an expressive API 23 | - Has a wealth of plug-ins 24 | 25 | Through plug-ins you can easily implement things like: 26 | - Multithreaded crawl 27 | - Crawl JavaScript dynamic rendering page (PhantomJS/headless WebKit) 28 | - Image downloads to local 29 | - Simulate browser behavior such as submitting Form forms 30 | - Web crawler 31 | - ..... 32 | 33 | ## Requirements 34 | - PHP >= 8.1 35 | 36 | ## Installation 37 | By Composer installation: 38 | ``` 39 | composer require jaeger/querylist 40 | ``` 41 | 42 | ## Usage 43 | 44 | #### DOM Traversal and Manipulation 45 | - Crawl「GitHub」all picture links 46 | 47 | ```php 48 | QueryList::get('https://github.com')->find('img')->attrs('src'); 49 | ``` 50 | - Crawl Google search results 51 | 52 | ```php 53 | $ql = QueryList::get('https://www.google.co.jp/search?q=QueryList'); 54 | 55 | $ql->find('title')->text(); //The page title 56 | $ql->find('meta[name=keywords]')->content; //The page keywords 57 | 58 | $ql->find('h3>a')->texts(); //Get a list of search results titles 59 | $ql->find('h3>a')->attrs('href'); //Get a list of search results links 60 | 61 | $ql->find('img')->src; //Gets the link address of the first image 62 | $ql->find('img:eq(1)')->src; //Gets the link address of the second image 63 | $ql->find('img')->eq(2)->src; //Gets the link address of the third image 64 | // Loop all the images 65 | $ql->find('img')->map(function($img){ 66 | echo $img->alt; //Print the alt attribute of the image 67 | }); 68 | ``` 69 | - More usage 70 | 71 | ```php 72 | $ql->find('#head')->append('
Append content
')->find('div')->htmls(); 73 | $ql->find('.two')->children('img')->attrs('alt'); // Get the class is the "two" element under all img child nodes 74 | // Loop class is the "two" element under all child nodes 75 | $data = $ql->find('.two')->children()->map(function ($item){ 76 | // Use "is" to determine the node type 77 | if($item->is('a')){ 78 | return $item->text(); 79 | }elseif($item->is('img')) 80 | { 81 | return $item->alt; 82 | } 83 | }); 84 | 85 | $ql->find('a')->attr('href', 'newVal')->removeClass('className')->html('newHtml')->... 86 | $ql->find('div > p')->add('div > ul')->filter(':has(a)')->find('p:first')->nextAll()->andSelf()->... 87 | $ql->find('div.old')->replaceWith( $ql->find('div.new')->clone())->appendTo('.trash')->prepend('Deleted')->... 88 | ``` 89 | #### List crawl 90 | Crawl the title and link of the Google search results list: 91 | ```php 92 | $data = QueryList::get('https://www.google.co.jp/search?q=QueryList') 93 | // Set the crawl rules 94 | ->rules([ 95 | 'title'=>array('h3','text'), 96 | 'link'=>array('h3>a','href') 97 | ]) 98 | ->query()->getData(); 99 | 100 | print_r($data->all()); 101 | ``` 102 | Results: 103 | ``` 104 | Array 105 | ( 106 | [0] => Array 107 | ( 108 | [title] => Angular - QueryList 109 | [link] => https://angular.io/api/core/QueryList 110 | ) 111 | [1] => Array 112 | ( 113 | [title] => QueryList | @angular/core - Angularリファレンス - Web Creative Park 114 | [link] => http://www.webcreativepark.net/angular/querylist/ 115 | ) 116 | [2] => Array 117 | ( 118 | [title] => QueryListにQueryを追加したり、追加されたことを感知する | TIPS ... 119 | [link] => http://www.webcreativepark.net/angular/querylist_query_add_subscribe/ 120 | ) 121 | //... 122 | ) 123 | ``` 124 | #### Encode convert 125 | ```php 126 | // Out charset :UTF-8 127 | // In charset :GB2312 128 | QueryList::get('https://top.etao.com')->encoding('UTF-8','GB2312')->find('a')->texts(); 129 | 130 | // Out charset:UTF-8 131 | // In charset:Automatic Identification 132 | QueryList::get('https://top.etao.com')->encoding('UTF-8')->find('a')->texts(); 133 | ``` 134 | 135 | #### HTTP Client (GuzzleHttp) 136 | - Carry cookie login GitHub 137 | ```php 138 | //Crawl GitHub content 139 | $ql = QueryList::get('https://github.com','param1=testvalue & params2=somevalue',[ 140 | 'headers' => [ 141 | // Fill in the cookie from the browser 142 | 'Cookie' => 'SINAGLOBAL=546064; wb_cmtLike_2112031=1; wvr=6;....' 143 | ] 144 | ]); 145 | //echo $ql->getHtml(); 146 | $userName = $ql->find('.header-nav-current-user>.css-truncate-target')->text(); 147 | echo $userName; 148 | ``` 149 | - Use the Http proxy 150 | ```php 151 | $urlParams = ['param1' => 'testvalue','params2' => 'somevalue']; 152 | $opts = [ 153 | // Set the http proxy 154 | 'proxy' => 'http://222.141.11.17:8118', 155 | //Set the timeout time in seconds 156 | 'timeout' => 30, 157 | // Fake HTTP headers 158 | 'headers' => [ 159 | 'Referer' => 'https://querylist.cc/', 160 | 'User-Agent' => 'testing/1.0', 161 | 'Accept' => 'application/json', 162 | 'X-Foo' => ['Bar', 'Baz'], 163 | 'Cookie' => 'abc=111;xxx=222' 164 | ] 165 | ]; 166 | $ql->get('http://httpbin.org/get',$urlParams,$opts); 167 | // echo $ql->getHtml(); 168 | ``` 169 | 170 | - Analog login 171 | ```php 172 | // Post login 173 | $ql = QueryList::post('http://xxxx.com/login',[ 174 | 'username' => 'admin', 175 | 'password' => '123456' 176 | ])->get('http://xxx.com/admin'); 177 | // Crawl pages that need to be logged in to access 178 | $ql->get('http://xxx.com/admin/page'); 179 | //echo $ql->getHtml(); 180 | ``` 181 | 182 | #### Submit forms 183 | Login GitHub 184 | ```php 185 | // Get the QueryList instance 186 | $ql = QueryList::getInstance(); 187 | // Get the login form 188 | $form = $ql->get('https://github.com/login')->find('form'); 189 | 190 | // Fill in the GitHub username and password 191 | $form->find('input[name=login]')->val('your github username or email'); 192 | $form->find('input[name=password]')->val('your github password'); 193 | 194 | // Serialize the form data 195 | $fromData = $form->serializeArray(); 196 | $postData = []; 197 | foreach ($fromData as $item) { 198 | $postData[$item['name']] = $item['value']; 199 | } 200 | 201 | // Submit the login form 202 | $actionUrl = 'https://github.com'.$form->attr('action'); 203 | $ql->post($actionUrl,$postData); 204 | // To determine whether the login is successful 205 | // echo $ql->getHtml(); 206 | $userName = $ql->find('.header-nav-current-user>.css-truncate-target')->text(); 207 | if($userName) 208 | { 209 | echo 'Login successful ! Welcome:'.$userName; 210 | }else{ 211 | echo 'Login failed !'; 212 | } 213 | ``` 214 | #### Bind function extension 215 | Customize the extension of a `myHttp` method: 216 | ```php 217 | $ql = QueryList::getInstance(); 218 | 219 | //Bind a `myHttp` method to the QueryList object 220 | $ql->bind('myHttp',function ($url){ 221 | // $this is the current QueryList object 222 | $html = file_get_contents($url); 223 | $this->setHtml($html); 224 | return $this; 225 | }); 226 | 227 | // And then you can call by the name of the binding 228 | $data = $ql->myHttp('https://toutiao.io')->find('h3 a')->texts(); 229 | print_r($data->all()); 230 | ``` 231 | Or package to class, and then bind: 232 | ```php 233 | $ql->bind('myHttp',function ($url){ 234 | return new MyHttp($this,$url); 235 | }); 236 | ``` 237 | 238 | #### Plugin used 239 | - Use the PhantomJS plugin to crawl JavaScript dynamically rendered pages: 240 | 241 | ```php 242 | // Set the PhantomJS binary file path during installation 243 | $ql = QueryList::use(PhantomJs::class,'/usr/local/bin/phantomjs'); 244 | 245 | // Crawl「500px」all picture links 246 | $data = $ql->browser('https://500px.com/editors')->find('img')->attrs('src'); 247 | print_r($data->all()); 248 | 249 | // Use the HTTP proxy 250 | $ql->browser('https://500px.com/editors',false,[ 251 | '--proxy' => '192.168.1.42:8080', 252 | '--proxy-type' => 'http' 253 | ]) 254 | ``` 255 | 256 | - Using the CURL multithreading plug-in, multi-threaded crawling GitHub trending : 257 | 258 | ```php 259 | $ql = QueryList::use(CurlMulti::class); 260 | $ql->curlMulti([ 261 | 'https://github.com/trending/php', 262 | 'https://github.com/trending/go', 263 | //.....more urls 264 | ]) 265 | // Called if task is success 266 | ->success(function (QueryList $ql,CurlMulti $curl,$r){ 267 | echo "Current url:{$r['info']['url']} \r\n"; 268 | $data = $ql->find('h3 a')->texts(); 269 | print_r($data->all()); 270 | }) 271 | // Task fail callback 272 | ->error(function ($errorInfo,CurlMulti $curl){ 273 | echo "Current url:{$errorInfo['info']['url']} \r\n"; 274 | print_r($errorInfo['error']); 275 | }) 276 | ->start([ 277 | // Maximum number of threads 278 | 'maxThread' => 10, 279 | // Number of error retries 280 | 'maxTry' => 3, 281 | ]); 282 | 283 | ``` 284 | 285 | ## Plugins 286 | - [jae-jae/QueryList-PhantomJS](https://github.com/jae-jae/QueryList-PhantomJS):Use PhantomJS to crawl Javascript dynamically rendered page. 287 | - [jae-jae/QueryList-CurlMulti](https://github.com/jae-jae/QueryList-CurlMulti) : Curl multi threading. 288 | - [jae-jae/QueryList-AbsoluteUrl](https://github.com/jae-jae/QueryList-AbsoluteUrl) : Converting relative urls to absolute. 289 | - [jae-jae/QueryList-Rule-Google](https://github.com/jae-jae/QueryList-Rule-Google) : Google searcher. 290 | - [jae-jae/QueryList-Rule-Baidu](https://github.com/jae-jae/QueryList-Rule-Baidu) : Baidu searcher. 291 | 292 | 293 | View more QueryList plugins and QueryList-based products: [QueryList Community](https://github.com/jae-jae/QueryList-Community) 294 | 295 | ## Contributing 296 | Welcome to contribute code for the QueryList。About Contributing Plugins can be viewed:[QueryList Plugin Contributing Guide](https://github.com/jae-jae/QueryList-Community/blob/master/CONTRIBUTING.md) 297 | 298 | ## Author 299 | Jaeger 300 | 301 | If this library is useful for you, say thanks [buying me a beer :beer:](https://www.paypal.me/jaepay)! 302 | 303 | ## Lisence 304 | QueryList is licensed under the license of MIT. See the LICENSE for more details. 305 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jaeger/querylist", 3 | "description": "Simple, elegant, extensible PHP Web Scraper (crawler/spider),Use the css3 dom selector,Based on phpQuery! 简洁、优雅、可扩展的PHP采集工具(爬虫),基于phpQuery。", 4 | "keywords":["QueryList","phpQuery","spider"], 5 | "homepage": "http://querylist.cc", 6 | "require": { 7 | "PHP":">=8.1", 8 | "jaeger/phpquery-single": "^1", 9 | "ext-dom": "*", 10 | "symfony/var-dumper": ">3.4", 11 | "jaeger/g-http": "^2.0" 12 | }, 13 | "suggest":{ 14 | 15 | }, 16 | "license": "MIT", 17 | "authors": [ 18 | { 19 | "name": "Jaeger", 20 | "email": "JaegerCode@gmail.com" 21 | } 22 | ], 23 | "autoload":{ 24 | "psr-4":{ 25 | "QL\\":"src" 26 | }, 27 | "files": [ 28 | "src/Collect/Support/helpers.php", 29 | "src/Collect/Support/alias.php" 30 | ] 31 | }, 32 | "autoload-dev": { 33 | "files": [ 34 | "src/Collect/Support/helpers.php", 35 | "src/Collect/Support/alias.php" 36 | ], 37 | "psr-4": { 38 | "Tests\\": "tests/" 39 | } 40 | }, 41 | "require-dev": { 42 | "phpunit/phpunit": "^8.5" 43 | }, 44 | "scripts": { 45 | "test": "./vendor/bin/phpunit" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Collect/Conditionable/HigherOrderWhenProxy.php: -------------------------------------------------------------------------------- 1 | target = $target; 44 | } 45 | 46 | /** 47 | * Set the condition on the proxy. 48 | * 49 | * @param bool $condition 50 | * @return $this 51 | */ 52 | public function condition($condition) 53 | { 54 | [$this->condition, $this->hasCondition] = [$condition, true]; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Indicate that the condition should be negated. 61 | * 62 | * @return $this 63 | */ 64 | public function negateConditionOnCapture() 65 | { 66 | $this->negateConditionOnCapture = true; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Proxy accessing an attribute onto the target. 73 | * 74 | * @param string $key 75 | * @return mixed 76 | */ 77 | public function __get($key) 78 | { 79 | if (! $this->hasCondition) { 80 | $condition = $this->target->{$key}; 81 | 82 | return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition); 83 | } 84 | 85 | return $this->condition 86 | ? $this->target->{$key} 87 | : $this->target; 88 | } 89 | 90 | /** 91 | * Proxy a method call on the target. 92 | * 93 | * @param string $method 94 | * @param array $parameters 95 | * @return mixed 96 | */ 97 | public function __call($method, $parameters) 98 | { 99 | if (! $this->hasCondition) { 100 | $condition = $this->target->{$method}(...$parameters); 101 | 102 | return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition); 103 | } 104 | 105 | return $this->condition 106 | ? $this->target->{$method}(...$parameters) 107 | : $this->target; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Collect/Contracts/Support/Arrayable.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function toArray(); 17 | } 18 | -------------------------------------------------------------------------------- /src/Collect/Contracts/Support/CanBeEscapedWhenCastToString.php: -------------------------------------------------------------------------------- 1 | all(); 55 | } elseif (! is_array($values)) { 56 | continue; 57 | } 58 | 59 | $results[] = $values; 60 | } 61 | 62 | return array_merge([], ...$results); 63 | } 64 | 65 | /** 66 | * Cross join the given arrays, returning all possible permutations. 67 | * 68 | * @param iterable ...$arrays 69 | * @return array 70 | */ 71 | public static function crossJoin(...$arrays) 72 | { 73 | $results = [[]]; 74 | 75 | foreach ($arrays as $index => $array) { 76 | $append = []; 77 | 78 | foreach ($results as $product) { 79 | foreach ($array as $item) { 80 | $product[$index] = $item; 81 | 82 | $append[] = $product; 83 | } 84 | } 85 | 86 | $results = $append; 87 | } 88 | 89 | return $results; 90 | } 91 | 92 | /** 93 | * Divide an array into two arrays. One with keys and the other with values. 94 | * 95 | * @param array $array 96 | * @return array 97 | */ 98 | public static function divide($array) 99 | { 100 | return [array_keys($array), array_values($array)]; 101 | } 102 | 103 | /** 104 | * Flatten a multi-dimensional associative array with dots. 105 | * 106 | * @param iterable $array 107 | * @param string $prepend 108 | * @return array 109 | */ 110 | public static function dot($array, $prepend = '') 111 | { 112 | $results = []; 113 | 114 | foreach ($array as $key => $value) { 115 | if (is_array($value) && ! empty($value)) { 116 | $results = array_merge($results, static::dot($value, $prepend.$key.'.')); 117 | } else { 118 | $results[$prepend.$key] = $value; 119 | } 120 | } 121 | 122 | return $results; 123 | } 124 | 125 | /** 126 | * Convert a flatten "dot" notation array into an expanded array. 127 | * 128 | * @param iterable $array 129 | * @return array 130 | */ 131 | public static function undot($array) 132 | { 133 | $results = []; 134 | 135 | foreach ($array as $key => $value) { 136 | static::set($results, $key, $value); 137 | } 138 | 139 | return $results; 140 | } 141 | 142 | /** 143 | * Get all of the given array except for a specified array of keys. 144 | * 145 | * @param array $array 146 | * @param array|string|int|float $keys 147 | * @return array 148 | */ 149 | public static function except($array, $keys) 150 | { 151 | static::forget($array, $keys); 152 | 153 | return $array; 154 | } 155 | 156 | /** 157 | * Determine if the given key exists in the provided array. 158 | * 159 | * @param \ArrayAccess|array $array 160 | * @param string|int $key 161 | * @return bool 162 | */ 163 | public static function exists($array, $key) 164 | { 165 | if ($array instanceof Enumerable) { 166 | return $array->has($key); 167 | } 168 | 169 | if ($array instanceof ArrayAccess) { 170 | return $array->offsetExists($key); 171 | } 172 | 173 | if (is_float($key)) { 174 | $key = (string) $key; 175 | } 176 | 177 | return array_key_exists($key, $array); 178 | } 179 | 180 | /** 181 | * Return the first element in an array passing a given truth test. 182 | * 183 | * @param iterable $array 184 | * @param callable|null $callback 185 | * @param mixed $default 186 | * @return mixed 187 | */ 188 | public static function first($array, callable $callback = null, $default = null) 189 | { 190 | if (is_null($callback)) { 191 | if (empty($array)) { 192 | return value($default); 193 | } 194 | 195 | foreach ($array as $item) { 196 | return $item; 197 | } 198 | } 199 | 200 | foreach ($array as $key => $value) { 201 | if ($callback($value, $key)) { 202 | return $value; 203 | } 204 | } 205 | 206 | return value($default); 207 | } 208 | 209 | /** 210 | * Return the last element in an array passing a given truth test. 211 | * 212 | * @param array $array 213 | * @param callable|null $callback 214 | * @param mixed $default 215 | * @return mixed 216 | */ 217 | public static function last($array, callable $callback = null, $default = null) 218 | { 219 | if (is_null($callback)) { 220 | return empty($array) ? value($default) : end($array); 221 | } 222 | 223 | return static::first(array_reverse($array, true), $callback, $default); 224 | } 225 | 226 | /** 227 | * Flatten a multi-dimensional array into a single level. 228 | * 229 | * @param iterable $array 230 | * @param int $depth 231 | * @return array 232 | */ 233 | public static function flatten($array, $depth = INF) 234 | { 235 | $result = []; 236 | 237 | foreach ($array as $item) { 238 | $item = $item instanceof Collection ? $item->all() : $item; 239 | 240 | if (! is_array($item)) { 241 | $result[] = $item; 242 | } else { 243 | $values = $depth === 1 244 | ? array_values($item) 245 | : static::flatten($item, $depth - 1); 246 | 247 | foreach ($values as $value) { 248 | $result[] = $value; 249 | } 250 | } 251 | } 252 | 253 | return $result; 254 | } 255 | 256 | /** 257 | * Remove one or many array items from a given array using "dot" notation. 258 | * 259 | * @param array $array 260 | * @param array|string|int|float $keys 261 | * @return void 262 | */ 263 | public static function forget(&$array, $keys) 264 | { 265 | $original = &$array; 266 | 267 | $keys = (array) $keys; 268 | 269 | if (count($keys) === 0) { 270 | return; 271 | } 272 | 273 | foreach ($keys as $key) { 274 | // if the exact key exists in the top-level, remove it 275 | if (static::exists($array, $key)) { 276 | unset($array[$key]); 277 | 278 | continue; 279 | } 280 | 281 | $parts = explode('.', $key); 282 | 283 | // clean up before each pass 284 | $array = &$original; 285 | 286 | while (count($parts) > 1) { 287 | $part = array_shift($parts); 288 | 289 | if (isset($array[$part]) && static::accessible($array[$part])) { 290 | $array = &$array[$part]; 291 | } else { 292 | continue 2; 293 | } 294 | } 295 | 296 | unset($array[array_shift($parts)]); 297 | } 298 | } 299 | 300 | /** 301 | * Get an item from an array using "dot" notation. 302 | * 303 | * @param \ArrayAccess|array $array 304 | * @param string|int|null $key 305 | * @param mixed $default 306 | * @return mixed 307 | */ 308 | public static function get($array, $key, $default = null) 309 | { 310 | if (! static::accessible($array)) { 311 | return value($default); 312 | } 313 | 314 | if (is_null($key)) { 315 | return $array; 316 | } 317 | 318 | if (static::exists($array, $key)) { 319 | return $array[$key]; 320 | } 321 | 322 | if (! str_contains($key, '.')) { 323 | return $array[$key] ?? value($default); 324 | } 325 | 326 | foreach (explode('.', $key) as $segment) { 327 | if (static::accessible($array) && static::exists($array, $segment)) { 328 | $array = $array[$segment]; 329 | } else { 330 | return value($default); 331 | } 332 | } 333 | 334 | return $array; 335 | } 336 | 337 | /** 338 | * Check if an item or items exist in an array using "dot" notation. 339 | * 340 | * @param \ArrayAccess|array $array 341 | * @param string|array $keys 342 | * @return bool 343 | */ 344 | public static function has($array, $keys) 345 | { 346 | $keys = (array) $keys; 347 | 348 | if (! $array || $keys === []) { 349 | return false; 350 | } 351 | 352 | foreach ($keys as $key) { 353 | $subKeyArray = $array; 354 | 355 | if (static::exists($array, $key)) { 356 | continue; 357 | } 358 | 359 | foreach (explode('.', $key) as $segment) { 360 | if (static::accessible($subKeyArray) && static::exists($subKeyArray, $segment)) { 361 | $subKeyArray = $subKeyArray[$segment]; 362 | } else { 363 | return false; 364 | } 365 | } 366 | } 367 | 368 | return true; 369 | } 370 | 371 | /** 372 | * Determine if any of the keys exist in an array using "dot" notation. 373 | * 374 | * @param \ArrayAccess|array $array 375 | * @param string|array $keys 376 | * @return bool 377 | */ 378 | public static function hasAny($array, $keys) 379 | { 380 | if (is_null($keys)) { 381 | return false; 382 | } 383 | 384 | $keys = (array) $keys; 385 | 386 | if (! $array) { 387 | return false; 388 | } 389 | 390 | if ($keys === []) { 391 | return false; 392 | } 393 | 394 | foreach ($keys as $key) { 395 | if (static::has($array, $key)) { 396 | return true; 397 | } 398 | } 399 | 400 | return false; 401 | } 402 | 403 | /** 404 | * Determines if an array is associative. 405 | * 406 | * An array is "associative" if it doesn't have sequential numerical keys beginning with zero. 407 | * 408 | * @param array $array 409 | * @return bool 410 | */ 411 | public static function isAssoc(array $array) 412 | { 413 | $keys = array_keys($array); 414 | 415 | return array_keys($keys) !== $keys; 416 | } 417 | 418 | /** 419 | * Determines if an array is a list. 420 | * 421 | * An array is a "list" if all array keys are sequential integers starting from 0 with no gaps in between. 422 | * 423 | * @param array $array 424 | * @return bool 425 | */ 426 | public static function isList($array) 427 | { 428 | return ! self::isAssoc($array); 429 | } 430 | 431 | /** 432 | * Join all items using a string. The final items can use a separate glue string. 433 | * 434 | * @param array $array 435 | * @param string $glue 436 | * @param string $finalGlue 437 | * @return string 438 | */ 439 | public static function join($array, $glue, $finalGlue = '') 440 | { 441 | if ($finalGlue === '') { 442 | return implode($glue, $array); 443 | } 444 | 445 | if (count($array) === 0) { 446 | return ''; 447 | } 448 | 449 | if (count($array) === 1) { 450 | return end($array); 451 | } 452 | 453 | $finalItem = array_pop($array); 454 | 455 | return implode($glue, $array).$finalGlue.$finalItem; 456 | } 457 | 458 | /** 459 | * Key an associative array by a field or using a callback. 460 | * 461 | * @param array $array 462 | * @param callable|array|string $keyBy 463 | * @return array 464 | */ 465 | public static function keyBy($array, $keyBy) 466 | { 467 | return Collection::make($array)->keyBy($keyBy)->all(); 468 | } 469 | 470 | /** 471 | * Prepend the key names of an associative array. 472 | * 473 | * @param array $array 474 | * @param string $prependWith 475 | * @return array 476 | */ 477 | public static function prependKeysWith($array, $prependWith) 478 | { 479 | return Collection::make($array)->mapWithKeys(function ($item, $key) use ($prependWith) { 480 | return [$prependWith.$key => $item]; 481 | })->all(); 482 | } 483 | 484 | /** 485 | * Get a subset of the items from the given array. 486 | * 487 | * @param array $array 488 | * @param array|string $keys 489 | * @return array 490 | */ 491 | public static function only($array, $keys) 492 | { 493 | return array_intersect_key($array, array_flip((array) $keys)); 494 | } 495 | 496 | /** 497 | * Pluck an array of values from an array. 498 | * 499 | * @param iterable $array 500 | * @param string|array|int|null $value 501 | * @param string|array|null $key 502 | * @return array 503 | */ 504 | public static function pluck($array, $value, $key = null) 505 | { 506 | $results = []; 507 | 508 | [$value, $key] = static::explodePluckParameters($value, $key); 509 | 510 | foreach ($array as $item) { 511 | $itemValue = data_get($item, $value); 512 | 513 | // If the key is "null", we will just append the value to the array and keep 514 | // looping. Otherwise we will key the array using the value of the key we 515 | // received from the developer. Then we'll return the final array form. 516 | if (is_null($key)) { 517 | $results[] = $itemValue; 518 | } else { 519 | $itemKey = data_get($item, $key); 520 | 521 | if (is_object($itemKey) && method_exists($itemKey, '__toString')) { 522 | $itemKey = (string) $itemKey; 523 | } 524 | 525 | $results[$itemKey] = $itemValue; 526 | } 527 | } 528 | 529 | return $results; 530 | } 531 | 532 | /** 533 | * Explode the "value" and "key" arguments passed to "pluck". 534 | * 535 | * @param string|array $value 536 | * @param string|array|null $key 537 | * @return array 538 | */ 539 | protected static function explodePluckParameters($value, $key) 540 | { 541 | $value = is_string($value) ? explode('.', $value) : $value; 542 | 543 | $key = is_null($key) || is_array($key) ? $key : explode('.', $key); 544 | 545 | return [$value, $key]; 546 | } 547 | 548 | /** 549 | * Run a map over each of the items in the array. 550 | * 551 | * @param array $array 552 | * @param callable $callback 553 | * @return array 554 | */ 555 | public static function map(array $array, callable $callback) 556 | { 557 | $keys = array_keys($array); 558 | 559 | try { 560 | $items = array_map($callback, $array, $keys); 561 | } catch (ArgumentCountError) { 562 | $items = array_map($callback, $array); 563 | } 564 | 565 | return array_combine($keys, $items); 566 | } 567 | 568 | /** 569 | * Push an item onto the beginning of an array. 570 | * 571 | * @param array $array 572 | * @param mixed $value 573 | * @param mixed $key 574 | * @return array 575 | */ 576 | public static function prepend($array, $value, $key = null) 577 | { 578 | if (func_num_args() == 2) { 579 | array_unshift($array, $value); 580 | } else { 581 | $array = [$key => $value] + $array; 582 | } 583 | 584 | return $array; 585 | } 586 | 587 | /** 588 | * Get a value from the array, and remove it. 589 | * 590 | * @param array $array 591 | * @param string|int $key 592 | * @param mixed $default 593 | * @return mixed 594 | */ 595 | public static function pull(&$array, $key, $default = null) 596 | { 597 | $value = static::get($array, $key, $default); 598 | 599 | static::forget($array, $key); 600 | 601 | return $value; 602 | } 603 | 604 | /** 605 | * Convert the array into a query string. 606 | * 607 | * @param array $array 608 | * @return string 609 | */ 610 | public static function query($array) 611 | { 612 | return http_build_query($array, '', '&', PHP_QUERY_RFC3986); 613 | } 614 | 615 | /** 616 | * Get one or a specified number of random values from an array. 617 | * 618 | * @param array $array 619 | * @param int|null $number 620 | * @param bool $preserveKeys 621 | * @return mixed 622 | * 623 | * @throws \InvalidArgumentException 624 | */ 625 | public static function random($array, $number = null, $preserveKeys = false) 626 | { 627 | $requested = is_null($number) ? 1 : $number; 628 | 629 | $count = count($array); 630 | 631 | if ($requested > $count) { 632 | throw new InvalidArgumentException( 633 | "You requested {$requested} items, but there are only {$count} items available." 634 | ); 635 | } 636 | 637 | if (is_null($number)) { 638 | return $array[array_rand($array)]; 639 | } 640 | 641 | if ((int) $number === 0) { 642 | return []; 643 | } 644 | 645 | $keys = array_rand($array, $number); 646 | 647 | $results = []; 648 | 649 | if ($preserveKeys) { 650 | foreach ((array) $keys as $key) { 651 | $results[$key] = $array[$key]; 652 | } 653 | } else { 654 | foreach ((array) $keys as $key) { 655 | $results[] = $array[$key]; 656 | } 657 | } 658 | 659 | return $results; 660 | } 661 | 662 | /** 663 | * Set an array item to a given value using "dot" notation. 664 | * 665 | * If no key is given to the method, the entire array will be replaced. 666 | * 667 | * @param array $array 668 | * @param string|int|null $key 669 | * @param mixed $value 670 | * @return array 671 | */ 672 | public static function set(&$array, $key, $value) 673 | { 674 | if (is_null($key)) { 675 | return $array = $value; 676 | } 677 | 678 | $keys = explode('.', $key); 679 | 680 | foreach ($keys as $i => $key) { 681 | if (count($keys) === 1) { 682 | break; 683 | } 684 | 685 | unset($keys[$i]); 686 | 687 | // If the key doesn't exist at this depth, we will just create an empty array 688 | // to hold the next value, allowing us to create the arrays to hold final 689 | // values at the correct depth. Then we'll keep digging into the array. 690 | if (! isset($array[$key]) || ! is_array($array[$key])) { 691 | $array[$key] = []; 692 | } 693 | 694 | $array = &$array[$key]; 695 | } 696 | 697 | $array[array_shift($keys)] = $value; 698 | 699 | return $array; 700 | } 701 | 702 | /** 703 | * Shuffle the given array and return the result. 704 | * 705 | * @param array $array 706 | * @param int|null $seed 707 | * @return array 708 | */ 709 | public static function shuffle($array, $seed = null) 710 | { 711 | if (is_null($seed)) { 712 | shuffle($array); 713 | } else { 714 | mt_srand($seed); 715 | shuffle($array); 716 | mt_srand(); 717 | } 718 | 719 | return $array; 720 | } 721 | 722 | /** 723 | * Sort the array using the given callback or "dot" notation. 724 | * 725 | * @param array $array 726 | * @param callable|array|string|null $callback 727 | * @return array 728 | */ 729 | public static function sort($array, $callback = null) 730 | { 731 | return Collection::make($array)->sortBy($callback)->all(); 732 | } 733 | 734 | /** 735 | * Sort the array in descending order using the given callback or "dot" notation. 736 | * 737 | * @param array $array 738 | * @param callable|array|string|null $callback 739 | * @return array 740 | */ 741 | public static function sortDesc($array, $callback = null) 742 | { 743 | return Collection::make($array)->sortByDesc($callback)->all(); 744 | } 745 | 746 | /** 747 | * Recursively sort an array by keys and values. 748 | * 749 | * @param array $array 750 | * @param int $options 751 | * @param bool $descending 752 | * @return array 753 | */ 754 | public static function sortRecursive($array, $options = SORT_REGULAR, $descending = false) 755 | { 756 | foreach ($array as &$value) { 757 | if (is_array($value)) { 758 | $value = static::sortRecursive($value, $options, $descending); 759 | } 760 | } 761 | 762 | if (static::isAssoc($array)) { 763 | $descending 764 | ? krsort($array, $options) 765 | : ksort($array, $options); 766 | } else { 767 | $descending 768 | ? rsort($array, $options) 769 | : sort($array, $options); 770 | } 771 | 772 | return $array; 773 | } 774 | 775 | /** 776 | * Conditionally compile classes from an array into a CSS class list. 777 | * 778 | * @param array $array 779 | * @return string 780 | */ 781 | public static function toCssClasses($array) 782 | { 783 | $classList = static::wrap($array); 784 | 785 | $classes = []; 786 | 787 | foreach ($classList as $class => $constraint) { 788 | if (is_numeric($class)) { 789 | $classes[] = $constraint; 790 | } elseif ($constraint) { 791 | $classes[] = $class; 792 | } 793 | } 794 | 795 | return implode(' ', $classes); 796 | } 797 | 798 | /** 799 | * Conditionally compile styles from an array into a style list. 800 | * 801 | * @param array $array 802 | * @return string 803 | */ 804 | public static function toCssStyles($array) 805 | { 806 | $styleList = static::wrap($array); 807 | 808 | $styles = []; 809 | 810 | foreach ($styleList as $class => $constraint) { 811 | if (is_numeric($class)) { 812 | $styles[] = Str::finish($constraint, ';'); 813 | } elseif ($constraint) { 814 | $styles[] = Str::finish($class, ';'); 815 | } 816 | } 817 | 818 | return implode(' ', $styles); 819 | } 820 | 821 | /** 822 | * Filter the array using the given callback. 823 | * 824 | * @param array $array 825 | * @param callable $callback 826 | * @return array 827 | */ 828 | public static function where($array, callable $callback) 829 | { 830 | return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH); 831 | } 832 | 833 | /** 834 | * Filter items where the value is not null. 835 | * 836 | * @param array $array 837 | * @return array 838 | */ 839 | public static function whereNotNull($array) 840 | { 841 | return static::where($array, fn ($value) => ! is_null($value)); 842 | } 843 | 844 | /** 845 | * If the given value is not an array and not null, wrap it in one. 846 | * 847 | * @param mixed $value 848 | * @return array 849 | */ 850 | public static function wrap($value) 851 | { 852 | if (is_null($value)) { 853 | return []; 854 | } 855 | 856 | return is_array($value) ? $value : [$value]; 857 | } 858 | } 859 | -------------------------------------------------------------------------------- /src/Collect/Support/Enumerable.php: -------------------------------------------------------------------------------- 1 | 18 | * @extends \IteratorAggregate 19 | */ 20 | interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable, JsonSerializable 21 | { 22 | /** 23 | * Create a new collection instance if the value isn't one already. 24 | * 25 | * @template TMakeKey of array-key 26 | * @template TMakeValue 27 | * 28 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable|null $items 29 | * @return static 30 | */ 31 | public static function make($items = []); 32 | 33 | /** 34 | * Create a new instance by invoking the callback a given amount of times. 35 | * 36 | * @param int $number 37 | * @param callable|null $callback 38 | * @return static 39 | */ 40 | public static function times($number, callable $callback = null); 41 | 42 | /** 43 | * Create a collection with the given range. 44 | * 45 | * @param int $from 46 | * @param int $to 47 | * @return static 48 | */ 49 | public static function range($from, $to); 50 | 51 | /** 52 | * Wrap the given value in a collection if applicable. 53 | * 54 | * @template TWrapValue 55 | * 56 | * @param iterable|TWrapValue $value 57 | * @return static 58 | */ 59 | public static function wrap($value); 60 | 61 | /** 62 | * Get the underlying items from the given collection if applicable. 63 | * 64 | * @template TUnwrapKey of array-key 65 | * @template TUnwrapValue 66 | * 67 | * @param array|static $value 68 | * @return array 69 | */ 70 | public static function unwrap($value); 71 | 72 | /** 73 | * Create a new instance with no items. 74 | * 75 | * @return static 76 | */ 77 | public static function empty(); 78 | 79 | /** 80 | * Get all items in the enumerable. 81 | * 82 | * @return array 83 | */ 84 | public function all(); 85 | 86 | /** 87 | * Alias for the "avg" method. 88 | * 89 | * @param (callable(TValue): float|int)|string|null $callback 90 | * @return float|int|null 91 | */ 92 | public function average($callback = null); 93 | 94 | /** 95 | * Get the median of a given key. 96 | * 97 | * @param string|array|null $key 98 | * @return float|int|null 99 | */ 100 | public function median($key = null); 101 | 102 | /** 103 | * Get the mode of a given key. 104 | * 105 | * @param string|array|null $key 106 | * @return array|null 107 | */ 108 | public function mode($key = null); 109 | 110 | /** 111 | * Collapse the items into a single enumerable. 112 | * 113 | * @return static 114 | */ 115 | public function collapse(); 116 | 117 | /** 118 | * Alias for the "contains" method. 119 | * 120 | * @param (callable(TValue, TKey): bool)|TValue|string $key 121 | * @param mixed $operator 122 | * @param mixed $value 123 | * @return bool 124 | */ 125 | public function some($key, $operator = null, $value = null); 126 | 127 | /** 128 | * Determine if an item exists, using strict comparison. 129 | * 130 | * @param (callable(TValue): bool)|TValue|array-key $key 131 | * @param TValue|null $value 132 | * @return bool 133 | */ 134 | public function containsStrict($key, $value = null); 135 | 136 | /** 137 | * Get the average value of a given key. 138 | * 139 | * @param (callable(TValue): float|int)|string|null $callback 140 | * @return float|int|null 141 | */ 142 | public function avg($callback = null); 143 | 144 | /** 145 | * Determine if an item exists in the enumerable. 146 | * 147 | * @param (callable(TValue, TKey): bool)|TValue|string $key 148 | * @param mixed $operator 149 | * @param mixed $value 150 | * @return bool 151 | */ 152 | public function contains($key, $operator = null, $value = null); 153 | 154 | /** 155 | * Determine if an item is not contained in the collection. 156 | * 157 | * @param mixed $key 158 | * @param mixed $operator 159 | * @param mixed $value 160 | * @return bool 161 | */ 162 | public function doesntContain($key, $operator = null, $value = null); 163 | 164 | /** 165 | * Cross join with the given lists, returning all possible permutations. 166 | * 167 | * @template TCrossJoinKey 168 | * @template TCrossJoinValue 169 | * 170 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable ...$lists 171 | * @return static> 172 | */ 173 | public function crossJoin(...$lists); 174 | 175 | /** 176 | * Dump the collection and end the script. 177 | * 178 | * @param mixed ...$args 179 | * @return never 180 | */ 181 | public function dd(...$args); 182 | 183 | /** 184 | * Dump the collection. 185 | * 186 | * @return $this 187 | */ 188 | public function dump(); 189 | 190 | /** 191 | * Get the items that are not present in the given items. 192 | * 193 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 194 | * @return static 195 | */ 196 | public function diff($items); 197 | 198 | /** 199 | * Get the items that are not present in the given items, using the callback. 200 | * 201 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 202 | * @param callable(TValue, TValue): int $callback 203 | * @return static 204 | */ 205 | public function diffUsing($items, callable $callback); 206 | 207 | /** 208 | * Get the items whose keys and values are not present in the given items. 209 | * 210 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 211 | * @return static 212 | */ 213 | public function diffAssoc($items); 214 | 215 | /** 216 | * Get the items whose keys and values are not present in the given items, using the callback. 217 | * 218 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 219 | * @param callable(TKey, TKey): int $callback 220 | * @return static 221 | */ 222 | public function diffAssocUsing($items, callable $callback); 223 | 224 | /** 225 | * Get the items whose keys are not present in the given items. 226 | * 227 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 228 | * @return static 229 | */ 230 | public function diffKeys($items); 231 | 232 | /** 233 | * Get the items whose keys are not present in the given items, using the callback. 234 | * 235 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 236 | * @param callable(TKey, TKey): int $callback 237 | * @return static 238 | */ 239 | public function diffKeysUsing($items, callable $callback); 240 | 241 | /** 242 | * Retrieve duplicate items. 243 | * 244 | * @param (callable(TValue): bool)|string|null $callback 245 | * @param bool $strict 246 | * @return static 247 | */ 248 | public function duplicates($callback = null, $strict = false); 249 | 250 | /** 251 | * Retrieve duplicate items using strict comparison. 252 | * 253 | * @param (callable(TValue): bool)|string|null $callback 254 | * @return static 255 | */ 256 | public function duplicatesStrict($callback = null); 257 | 258 | /** 259 | * Execute a callback over each item. 260 | * 261 | * @param callable(TValue, TKey): mixed $callback 262 | * @return $this 263 | */ 264 | public function each(callable $callback); 265 | 266 | /** 267 | * Execute a callback over each nested chunk of items. 268 | * 269 | * @param callable $callback 270 | * @return static 271 | */ 272 | public function eachSpread(callable $callback); 273 | 274 | /** 275 | * Determine if all items pass the given truth test. 276 | * 277 | * @param (callable(TValue, TKey): bool)|TValue|string $key 278 | * @param mixed $operator 279 | * @param mixed $value 280 | * @return bool 281 | */ 282 | public function every($key, $operator = null, $value = null); 283 | 284 | /** 285 | * Get all items except for those with the specified keys. 286 | * 287 | * @param \QL\Collect\Support\Enumerable|array $keys 288 | * @return static 289 | */ 290 | public function except($keys); 291 | 292 | /** 293 | * Run a filter over each of the items. 294 | * 295 | * @param (callable(TValue): bool)|null $callback 296 | * @return static 297 | */ 298 | public function filter(callable $callback = null); 299 | 300 | /** 301 | * Apply the callback if the given "value" is (or resolves to) truthy. 302 | * 303 | * @template TWhenReturnType as null 304 | * 305 | * @param bool $value 306 | * @param (callable($this): TWhenReturnType)|null $callback 307 | * @param (callable($this): TWhenReturnType)|null $default 308 | * @return $this|TWhenReturnType 309 | */ 310 | public function when($value, callable $callback = null, callable $default = null); 311 | 312 | /** 313 | * Apply the callback if the collection is empty. 314 | * 315 | * @template TWhenEmptyReturnType 316 | * 317 | * @param (callable($this): TWhenEmptyReturnType) $callback 318 | * @param (callable($this): TWhenEmptyReturnType)|null $default 319 | * @return $this|TWhenEmptyReturnType 320 | */ 321 | public function whenEmpty(callable $callback, callable $default = null); 322 | 323 | /** 324 | * Apply the callback if the collection is not empty. 325 | * 326 | * @template TWhenNotEmptyReturnType 327 | * 328 | * @param callable($this): TWhenNotEmptyReturnType $callback 329 | * @param (callable($this): TWhenNotEmptyReturnType)|null $default 330 | * @return $this|TWhenNotEmptyReturnType 331 | */ 332 | public function whenNotEmpty(callable $callback, callable $default = null); 333 | 334 | /** 335 | * Apply the callback if the given "value" is (or resolves to) truthy. 336 | * 337 | * @template TUnlessReturnType 338 | * 339 | * @param bool $value 340 | * @param (callable($this): TUnlessReturnType) $callback 341 | * @param (callable($this): TUnlessReturnType)|null $default 342 | * @return $this|TUnlessReturnType 343 | */ 344 | public function unless($value, callable $callback, callable $default = null); 345 | 346 | /** 347 | * Apply the callback unless the collection is empty. 348 | * 349 | * @template TUnlessEmptyReturnType 350 | * 351 | * @param callable($this): TUnlessEmptyReturnType $callback 352 | * @param (callable($this): TUnlessEmptyReturnType)|null $default 353 | * @return $this|TUnlessEmptyReturnType 354 | */ 355 | public function unlessEmpty(callable $callback, callable $default = null); 356 | 357 | /** 358 | * Apply the callback unless the collection is not empty. 359 | * 360 | * @template TUnlessNotEmptyReturnType 361 | * 362 | * @param callable($this): TUnlessNotEmptyReturnType $callback 363 | * @param (callable($this): TUnlessNotEmptyReturnType)|null $default 364 | * @return $this|TUnlessNotEmptyReturnType 365 | */ 366 | public function unlessNotEmpty(callable $callback, callable $default = null); 367 | 368 | /** 369 | * Filter items by the given key value pair. 370 | * 371 | * @param string $key 372 | * @param mixed $operator 373 | * @param mixed $value 374 | * @return static 375 | */ 376 | public function where($key, $operator = null, $value = null); 377 | 378 | /** 379 | * Filter items where the value for the given key is null. 380 | * 381 | * @param string|null $key 382 | * @return static 383 | */ 384 | public function whereNull($key = null); 385 | 386 | /** 387 | * Filter items where the value for the given key is not null. 388 | * 389 | * @param string|null $key 390 | * @return static 391 | */ 392 | public function whereNotNull($key = null); 393 | 394 | /** 395 | * Filter items by the given key value pair using strict comparison. 396 | * 397 | * @param string $key 398 | * @param mixed $value 399 | * @return static 400 | */ 401 | public function whereStrict($key, $value); 402 | 403 | /** 404 | * Filter items by the given key value pair. 405 | * 406 | * @param string $key 407 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 408 | * @param bool $strict 409 | * @return static 410 | */ 411 | public function whereIn($key, $values, $strict = false); 412 | 413 | /** 414 | * Filter items by the given key value pair using strict comparison. 415 | * 416 | * @param string $key 417 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 418 | * @return static 419 | */ 420 | public function whereInStrict($key, $values); 421 | 422 | /** 423 | * Filter items such that the value of the given key is between the given values. 424 | * 425 | * @param string $key 426 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 427 | * @return static 428 | */ 429 | public function whereBetween($key, $values); 430 | 431 | /** 432 | * Filter items such that the value of the given key is not between the given values. 433 | * 434 | * @param string $key 435 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 436 | * @return static 437 | */ 438 | public function whereNotBetween($key, $values); 439 | 440 | /** 441 | * Filter items by the given key value pair. 442 | * 443 | * @param string $key 444 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 445 | * @param bool $strict 446 | * @return static 447 | */ 448 | public function whereNotIn($key, $values, $strict = false); 449 | 450 | /** 451 | * Filter items by the given key value pair using strict comparison. 452 | * 453 | * @param string $key 454 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 455 | * @return static 456 | */ 457 | public function whereNotInStrict($key, $values); 458 | 459 | /** 460 | * Filter the items, removing any items that don't match the given type(s). 461 | * 462 | * @template TWhereInstanceOf 463 | * 464 | * @param class-string|array> $type 465 | * @return static 466 | */ 467 | public function whereInstanceOf($type); 468 | 469 | /** 470 | * Get the first item from the enumerable passing the given truth test. 471 | * 472 | * @template TFirstDefault 473 | * 474 | * @param (callable(TValue,TKey): bool)|null $callback 475 | * @param TFirstDefault|(\Closure(): TFirstDefault) $default 476 | * @return TValue|TFirstDefault 477 | */ 478 | public function first(callable $callback = null, $default = null); 479 | 480 | /** 481 | * Get the first item by the given key value pair. 482 | * 483 | * @param string $key 484 | * @param mixed $operator 485 | * @param mixed $value 486 | * @return TValue|null 487 | */ 488 | public function firstWhere($key, $operator = null, $value = null); 489 | 490 | /** 491 | * Get a flattened array of the items in the collection. 492 | * 493 | * @param int $depth 494 | * @return static 495 | */ 496 | public function flatten($depth = INF); 497 | 498 | /** 499 | * Flip the values with their keys. 500 | * 501 | * @return static 502 | */ 503 | public function flip(); 504 | 505 | /** 506 | * Get an item from the collection by key. 507 | * 508 | * @template TGetDefault 509 | * 510 | * @param TKey $key 511 | * @param TGetDefault|(\Closure(): TGetDefault) $default 512 | * @return TValue|TGetDefault 513 | */ 514 | public function get($key, $default = null); 515 | 516 | /** 517 | * Group an associative array by a field or using a callback. 518 | * 519 | * @param (callable(TValue, TKey): array-key)|array|string $groupBy 520 | * @param bool $preserveKeys 521 | * @return static> 522 | */ 523 | public function groupBy($groupBy, $preserveKeys = false); 524 | 525 | /** 526 | * Key an associative array by a field or using a callback. 527 | * 528 | * @param (callable(TValue, TKey): array-key)|array|string $keyBy 529 | * @return static 530 | */ 531 | public function keyBy($keyBy); 532 | 533 | /** 534 | * Determine if an item exists in the collection by key. 535 | * 536 | * @param TKey|array $key 537 | * @return bool 538 | */ 539 | public function has($key); 540 | 541 | /** 542 | * Determine if any of the keys exist in the collection. 543 | * 544 | * @param mixed $key 545 | * @return bool 546 | */ 547 | public function hasAny($key); 548 | 549 | /** 550 | * Concatenate values of a given key as a string. 551 | * 552 | * @param string $value 553 | * @param string|null $glue 554 | * @return string 555 | */ 556 | public function implode($value, $glue = null); 557 | 558 | /** 559 | * Intersect the collection with the given items. 560 | * 561 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 562 | * @return static 563 | */ 564 | public function intersect($items); 565 | 566 | /** 567 | * Intersect the collection with the given items by key. 568 | * 569 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 570 | * @return static 571 | */ 572 | public function intersectByKeys($items); 573 | 574 | /** 575 | * Determine if the collection is empty or not. 576 | * 577 | * @return bool 578 | */ 579 | public function isEmpty(); 580 | 581 | /** 582 | * Determine if the collection is not empty. 583 | * 584 | * @return bool 585 | */ 586 | public function isNotEmpty(); 587 | 588 | /** 589 | * Determine if the collection contains a single item. 590 | * 591 | * @return bool 592 | */ 593 | public function containsOneItem(); 594 | 595 | /** 596 | * Join all items from the collection using a string. The final items can use a separate glue string. 597 | * 598 | * @param string $glue 599 | * @param string $finalGlue 600 | * @return string 601 | */ 602 | public function join($glue, $finalGlue = ''); 603 | 604 | /** 605 | * Get the keys of the collection items. 606 | * 607 | * @return static 608 | */ 609 | public function keys(); 610 | 611 | /** 612 | * Get the last item from the collection. 613 | * 614 | * @template TLastDefault 615 | * 616 | * @param (callable(TValue, TKey): bool)|null $callback 617 | * @param TLastDefault|(\Closure(): TLastDefault) $default 618 | * @return TValue|TLastDefault 619 | */ 620 | public function last(callable $callback = null, $default = null); 621 | 622 | /** 623 | * Run a map over each of the items. 624 | * 625 | * @template TMapValue 626 | * 627 | * @param callable(TValue, TKey): TMapValue $callback 628 | * @return static 629 | */ 630 | public function map(callable $callback); 631 | 632 | /** 633 | * Run a map over each nested chunk of items. 634 | * 635 | * @param callable $callback 636 | * @return static 637 | */ 638 | public function mapSpread(callable $callback); 639 | 640 | /** 641 | * Run a dictionary map over the items. 642 | * 643 | * The callback should return an associative array with a single key/value pair. 644 | * 645 | * @template TMapToDictionaryKey of array-key 646 | * @template TMapToDictionaryValue 647 | * 648 | * @param callable(TValue, TKey): array $callback 649 | * @return static> 650 | */ 651 | public function mapToDictionary(callable $callback); 652 | 653 | /** 654 | * Run a grouping map over the items. 655 | * 656 | * The callback should return an associative array with a single key/value pair. 657 | * 658 | * @template TMapToGroupsKey of array-key 659 | * @template TMapToGroupsValue 660 | * 661 | * @param callable(TValue, TKey): array $callback 662 | * @return static> 663 | */ 664 | public function mapToGroups(callable $callback); 665 | 666 | /** 667 | * Run an associative map over each of the items. 668 | * 669 | * The callback should return an associative array with a single key/value pair. 670 | * 671 | * @template TMapWithKeysKey of array-key 672 | * @template TMapWithKeysValue 673 | * 674 | * @param callable(TValue, TKey): array $callback 675 | * @return static 676 | */ 677 | public function mapWithKeys(callable $callback); 678 | 679 | /** 680 | * Map a collection and flatten the result by a single level. 681 | * 682 | * @template TFlatMapKey of array-key 683 | * @template TFlatMapValue 684 | * 685 | * @param callable(TValue, TKey): (\QL\Collect\Support\Collection|array) $callback 686 | * @return static 687 | */ 688 | public function flatMap(callable $callback); 689 | 690 | /** 691 | * Map the values into a new class. 692 | * 693 | * @template TMapIntoValue 694 | * 695 | * @param class-string $class 696 | * @return static 697 | */ 698 | public function mapInto($class); 699 | 700 | /** 701 | * Merge the collection with the given items. 702 | * 703 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 704 | * @return static 705 | */ 706 | public function merge($items); 707 | 708 | /** 709 | * Recursively merge the collection with the given items. 710 | * 711 | * @template TMergeRecursiveValue 712 | * 713 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 714 | * @return static 715 | */ 716 | public function mergeRecursive($items); 717 | 718 | /** 719 | * Create a collection by using this collection for keys and another for its values. 720 | * 721 | * @template TCombineValue 722 | * 723 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 724 | * @return static 725 | */ 726 | public function combine($values); 727 | 728 | /** 729 | * Union the collection with the given items. 730 | * 731 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 732 | * @return static 733 | */ 734 | public function union($items); 735 | 736 | /** 737 | * Get the min value of a given key. 738 | * 739 | * @param (callable(TValue):mixed)|string|null $callback 740 | * @return mixed 741 | */ 742 | public function min($callback = null); 743 | 744 | /** 745 | * Get the max value of a given key. 746 | * 747 | * @param (callable(TValue):mixed)|string|null $callback 748 | * @return mixed 749 | */ 750 | public function max($callback = null); 751 | 752 | /** 753 | * Create a new collection consisting of every n-th element. 754 | * 755 | * @param int $step 756 | * @param int $offset 757 | * @return static 758 | */ 759 | public function nth($step, $offset = 0); 760 | 761 | /** 762 | * Get the items with the specified keys. 763 | * 764 | * @param \QL\Collect\Support\Enumerable|array|string $keys 765 | * @return static 766 | */ 767 | public function only($keys); 768 | 769 | /** 770 | * "Paginate" the collection by slicing it into a smaller collection. 771 | * 772 | * @param int $page 773 | * @param int $perPage 774 | * @return static 775 | */ 776 | public function forPage($page, $perPage); 777 | 778 | /** 779 | * Partition the collection into two arrays using the given callback or key. 780 | * 781 | * @param (callable(TValue, TKey): bool)|TValue|string $key 782 | * @param mixed $operator 783 | * @param mixed $value 784 | * @return static, static> 785 | */ 786 | public function partition($key, $operator = null, $value = null); 787 | 788 | /** 789 | * Push all of the given items onto the collection. 790 | * 791 | * @param iterable $source 792 | * @return static 793 | */ 794 | public function concat($source); 795 | 796 | /** 797 | * Get one or a specified number of items randomly from the collection. 798 | * 799 | * @param int|null $number 800 | * @return static|TValue 801 | * 802 | * @throws \InvalidArgumentException 803 | */ 804 | public function random($number = null); 805 | 806 | /** 807 | * Reduce the collection to a single value. 808 | * 809 | * @template TReduceInitial 810 | * @template TReduceReturnType 811 | * 812 | * @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback 813 | * @param TReduceInitial $initial 814 | * @return TReduceReturnType 815 | */ 816 | public function reduce(callable $callback, $initial = null); 817 | 818 | /** 819 | * Reduce the collection to multiple aggregate values. 820 | * 821 | * @param callable $callback 822 | * @param mixed ...$initial 823 | * @return array 824 | * 825 | * @throws \UnexpectedValueException 826 | */ 827 | public function reduceSpread(callable $callback, ...$initial); 828 | 829 | /** 830 | * Replace the collection items with the given items. 831 | * 832 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 833 | * @return static 834 | */ 835 | public function replace($items); 836 | 837 | /** 838 | * Recursively replace the collection items with the given items. 839 | * 840 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $items 841 | * @return static 842 | */ 843 | public function replaceRecursive($items); 844 | 845 | /** 846 | * Reverse items order. 847 | * 848 | * @return static 849 | */ 850 | public function reverse(); 851 | 852 | /** 853 | * Search the collection for a given value and return the corresponding key if successful. 854 | * 855 | * @param TValue|callable(TValue,TKey): bool $value 856 | * @param bool $strict 857 | * @return TKey|bool 858 | */ 859 | public function search($value, $strict = false); 860 | 861 | /** 862 | * Shuffle the items in the collection. 863 | * 864 | * @param int|null $seed 865 | * @return static 866 | */ 867 | public function shuffle($seed = null); 868 | 869 | /** 870 | * Create chunks representing a "sliding window" view of the items in the collection. 871 | * 872 | * @param int $size 873 | * @param int $step 874 | * @return static 875 | */ 876 | public function sliding($size = 2, $step = 1); 877 | 878 | /** 879 | * Skip the first {$count} items. 880 | * 881 | * @param int $count 882 | * @return static 883 | */ 884 | public function skip($count); 885 | 886 | /** 887 | * Skip items in the collection until the given condition is met. 888 | * 889 | * @param TValue|callable(TValue,TKey): bool $value 890 | * @return static 891 | */ 892 | public function skipUntil($value); 893 | 894 | /** 895 | * Skip items in the collection while the given condition is met. 896 | * 897 | * @param TValue|callable(TValue,TKey): bool $value 898 | * @return static 899 | */ 900 | public function skipWhile($value); 901 | 902 | /** 903 | * Get a slice of items from the enumerable. 904 | * 905 | * @param int $offset 906 | * @param int|null $length 907 | * @return static 908 | */ 909 | public function slice($offset, $length = null); 910 | 911 | /** 912 | * Split a collection into a certain number of groups. 913 | * 914 | * @param int $numberOfGroups 915 | * @return static 916 | */ 917 | public function split($numberOfGroups); 918 | 919 | /** 920 | * Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception. 921 | * 922 | * @param (callable(TValue, TKey): bool)|string $key 923 | * @param mixed $operator 924 | * @param mixed $value 925 | * @return TValue 926 | * 927 | * @throws \QL\Collect\Support\ItemNotFoundException 928 | * @throws \QL\Collect\Support\MultipleItemsFoundException 929 | */ 930 | public function sole($key = null, $operator = null, $value = null); 931 | 932 | /** 933 | * Get the first item in the collection but throw an exception if no matching items exist. 934 | * 935 | * @param (callable(TValue, TKey): bool)|string $key 936 | * @param mixed $operator 937 | * @param mixed $value 938 | * @return TValue 939 | * 940 | * @throws \QL\Collect\Support\ItemNotFoundException 941 | */ 942 | public function firstOrFail($key = null, $operator = null, $value = null); 943 | 944 | /** 945 | * Chunk the collection into chunks of the given size. 946 | * 947 | * @param int $size 948 | * @return static 949 | */ 950 | public function chunk($size); 951 | 952 | /** 953 | * Chunk the collection into chunks with a callback. 954 | * 955 | * @param callable(TValue, TKey, static): bool $callback 956 | * @return static> 957 | */ 958 | public function chunkWhile(callable $callback); 959 | 960 | /** 961 | * Split a collection into a certain number of groups, and fill the first groups completely. 962 | * 963 | * @param int $numberOfGroups 964 | * @return static 965 | */ 966 | public function splitIn($numberOfGroups); 967 | 968 | /** 969 | * Sort through each item with a callback. 970 | * 971 | * @param (callable(TValue, TValue): int)|null|int $callback 972 | * @return static 973 | */ 974 | public function sort($callback = null); 975 | 976 | /** 977 | * Sort items in descending order. 978 | * 979 | * @param int $options 980 | * @return static 981 | */ 982 | public function sortDesc($options = SORT_REGULAR); 983 | 984 | /** 985 | * Sort the collection using the given callback. 986 | * 987 | * @param array|(callable(TValue, TKey): mixed)|string $callback 988 | * @param int $options 989 | * @param bool $descending 990 | * @return static 991 | */ 992 | public function sortBy($callback, $options = SORT_REGULAR, $descending = false); 993 | 994 | /** 995 | * Sort the collection in descending order using the given callback. 996 | * 997 | * @param array|(callable(TValue, TKey): mixed)|string $callback 998 | * @param int $options 999 | * @return static 1000 | */ 1001 | public function sortByDesc($callback, $options = SORT_REGULAR); 1002 | 1003 | /** 1004 | * Sort the collection keys. 1005 | * 1006 | * @param int $options 1007 | * @param bool $descending 1008 | * @return static 1009 | */ 1010 | public function sortKeys($options = SORT_REGULAR, $descending = false); 1011 | 1012 | /** 1013 | * Sort the collection keys in descending order. 1014 | * 1015 | * @param int $options 1016 | * @return static 1017 | */ 1018 | public function sortKeysDesc($options = SORT_REGULAR); 1019 | 1020 | /** 1021 | * Sort the collection keys using a callback. 1022 | * 1023 | * @param callable(TKey, TKey): int $callback 1024 | * @return static 1025 | */ 1026 | public function sortKeysUsing(callable $callback); 1027 | 1028 | /** 1029 | * Get the sum of the given values. 1030 | * 1031 | * @param (callable(TValue): mixed)|string|null $callback 1032 | * @return mixed 1033 | */ 1034 | public function sum($callback = null); 1035 | 1036 | /** 1037 | * Take the first or last {$limit} items. 1038 | * 1039 | * @param int $limit 1040 | * @return static 1041 | */ 1042 | public function take($limit); 1043 | 1044 | /** 1045 | * Take items in the collection until the given condition is met. 1046 | * 1047 | * @param TValue|callable(TValue,TKey): bool $value 1048 | * @return static 1049 | */ 1050 | public function takeUntil($value); 1051 | 1052 | /** 1053 | * Take items in the collection while the given condition is met. 1054 | * 1055 | * @param TValue|callable(TValue,TKey): bool $value 1056 | * @return static 1057 | */ 1058 | public function takeWhile($value); 1059 | 1060 | /** 1061 | * Pass the collection to the given callback and then return it. 1062 | * 1063 | * @param callable(TValue): mixed $callback 1064 | * @return $this 1065 | */ 1066 | public function tap(callable $callback); 1067 | 1068 | /** 1069 | * Pass the enumerable to the given callback and return the result. 1070 | * 1071 | * @template TPipeReturnType 1072 | * 1073 | * @param callable($this): TPipeReturnType $callback 1074 | * @return TPipeReturnType 1075 | */ 1076 | public function pipe(callable $callback); 1077 | 1078 | /** 1079 | * Pass the collection into a new class. 1080 | * 1081 | * @param class-string $class 1082 | * @return mixed 1083 | */ 1084 | public function pipeInto($class); 1085 | 1086 | /** 1087 | * Pass the collection through a series of callable pipes and return the result. 1088 | * 1089 | * @param array $pipes 1090 | * @return mixed 1091 | */ 1092 | public function pipeThrough($pipes); 1093 | 1094 | /** 1095 | * Get the values of a given key. 1096 | * 1097 | * @param string|array $value 1098 | * @param string|null $key 1099 | * @return static 1100 | */ 1101 | public function pluck($value, $key = null); 1102 | 1103 | /** 1104 | * Create a collection of all elements that do not pass a given truth test. 1105 | * 1106 | * @param (callable(TValue, TKey): bool)|bool|TValue $callback 1107 | * @return static 1108 | */ 1109 | public function reject($callback = true); 1110 | 1111 | /** 1112 | * Convert a flatten "dot" notation array into an expanded array. 1113 | * 1114 | * @return static 1115 | */ 1116 | public function undot(); 1117 | 1118 | /** 1119 | * Return only unique items from the collection array. 1120 | * 1121 | * @param (callable(TValue, TKey): mixed)|string|null $key 1122 | * @param bool $strict 1123 | * @return static 1124 | */ 1125 | public function unique($key = null, $strict = false); 1126 | 1127 | /** 1128 | * Return only unique items from the collection array using strict comparison. 1129 | * 1130 | * @param (callable(TValue, TKey): mixed)|string|null $key 1131 | * @return static 1132 | */ 1133 | public function uniqueStrict($key = null); 1134 | 1135 | /** 1136 | * Reset the keys on the underlying array. 1137 | * 1138 | * @return static 1139 | */ 1140 | public function values(); 1141 | 1142 | /** 1143 | * Pad collection to the specified length with a value. 1144 | * 1145 | * @template TPadValue 1146 | * 1147 | * @param int $size 1148 | * @param TPadValue $value 1149 | * @return static 1150 | */ 1151 | public function pad($size, $value); 1152 | 1153 | /** 1154 | * Get the values iterator. 1155 | * 1156 | * @return \Traversable 1157 | */ 1158 | public function getIterator(): Traversable; 1159 | 1160 | /** 1161 | * Count the number of items in the collection. 1162 | * 1163 | * @return int 1164 | */ 1165 | public function count(): int; 1166 | 1167 | /** 1168 | * Count the number of items in the collection by a field or using a callback. 1169 | * 1170 | * @param (callable(TValue, TKey): array-key)|string|null $countBy 1171 | * @return static 1172 | */ 1173 | public function countBy($countBy = null); 1174 | 1175 | /** 1176 | * Zip the collection together with one or more arrays. 1177 | * 1178 | * e.g. new Collection([1, 2, 3])->zip([4, 5, 6]); 1179 | * => [[1, 4], [2, 5], [3, 6]] 1180 | * 1181 | * @template TZipValue 1182 | * 1183 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable ...$items 1184 | * @return static> 1185 | */ 1186 | public function zip($items); 1187 | 1188 | /** 1189 | * Collect the values into a collection. 1190 | * 1191 | * @return \QL\Collect\Support\Collection 1192 | */ 1193 | public function collect(); 1194 | 1195 | /** 1196 | * Get the collection of items as a plain array. 1197 | * 1198 | * @return array 1199 | */ 1200 | public function toArray(); 1201 | 1202 | /** 1203 | * Convert the object into something JSON serializable. 1204 | * 1205 | * @return mixed 1206 | */ 1207 | public function jsonSerialize(): mixed; 1208 | 1209 | /** 1210 | * Get the collection of items as JSON. 1211 | * 1212 | * @param int $options 1213 | * @return string 1214 | */ 1215 | public function toJson($options = 0); 1216 | 1217 | /** 1218 | * Get a CachingIterator instance. 1219 | * 1220 | * @param int $flags 1221 | * @return \CachingIterator 1222 | */ 1223 | public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING); 1224 | 1225 | /** 1226 | * Convert the collection to its string representation. 1227 | * 1228 | * @return string 1229 | */ 1230 | public function __toString(); 1231 | 1232 | /** 1233 | * Indicate that the model's string representation should be escaped when __toString is invoked. 1234 | * 1235 | * @param bool $escape 1236 | * @return $this 1237 | */ 1238 | public function escapeWhenCastingToString($escape = true); 1239 | 1240 | /** 1241 | * Add a method to the list of proxied methods. 1242 | * 1243 | * @param string $method 1244 | * @return void 1245 | */ 1246 | public static function proxy($method); 1247 | 1248 | /** 1249 | * Dynamically access collection proxies. 1250 | * 1251 | * @param string $key 1252 | * @return mixed 1253 | * 1254 | * @throws \Exception 1255 | */ 1256 | public function __get($key); 1257 | } 1258 | -------------------------------------------------------------------------------- /src/Collect/Support/HigherOrderCollectionProxy.php: -------------------------------------------------------------------------------- 1 | method = $method; 34 | $this->collection = $collection; 35 | } 36 | 37 | /** 38 | * Proxy accessing an attribute onto the collection items. 39 | * 40 | * @param string $key 41 | * @return mixed 42 | */ 43 | public function __get($key) 44 | { 45 | return $this->collection->{$this->method}(function ($value) use ($key) { 46 | return is_array($value) ? $value[$key] : $value->{$key}; 47 | }); 48 | } 49 | 50 | /** 51 | * Proxy a method call onto the collection items. 52 | * 53 | * @param string $method 54 | * @param array $parameters 55 | * @return mixed 56 | */ 57 | public function __call($method, $parameters) 58 | { 59 | return $this->collection->{$this->method}(function ($value) use ($method, $parameters) { 60 | return $value->{$method}(...$parameters); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Collect/Support/Str.php: -------------------------------------------------------------------------------- 1 | $needles 224 | * @param bool $ignoreCase 225 | * @return bool 226 | */ 227 | public static function contains($haystack, $needles, $ignoreCase = false) 228 | { 229 | if ($ignoreCase) { 230 | $haystack = mb_strtolower($haystack); 231 | } 232 | 233 | if (! is_iterable($needles)) { 234 | $needles = (array) $needles; 235 | } 236 | 237 | foreach ($needles as $needle) { 238 | if ($ignoreCase) { 239 | $needle = mb_strtolower($needle); 240 | } 241 | 242 | if ($needle !== '' && str_contains($haystack, $needle)) { 243 | return true; 244 | } 245 | } 246 | 247 | return false; 248 | } 249 | 250 | /** 251 | * Determine if a given string contains all array values. 252 | * 253 | * @param string $haystack 254 | * @param iterable $needles 255 | * @param bool $ignoreCase 256 | * @return bool 257 | */ 258 | public static function containsAll($haystack, $needles, $ignoreCase = false) 259 | { 260 | foreach ($needles as $needle) { 261 | if (! static::contains($haystack, $needle, $ignoreCase)) { 262 | return false; 263 | } 264 | } 265 | 266 | return true; 267 | } 268 | 269 | /** 270 | * Determine if a given string ends with a given substring. 271 | * 272 | * @param string $haystack 273 | * @param string|iterable $needles 274 | * @return bool 275 | */ 276 | public static function endsWith($haystack, $needles) 277 | { 278 | if (! is_iterable($needles)) { 279 | $needles = (array) $needles; 280 | } 281 | 282 | foreach ($needles as $needle) { 283 | if ((string) $needle !== '' && str_ends_with($haystack, $needle)) { 284 | return true; 285 | } 286 | } 287 | 288 | return false; 289 | } 290 | 291 | /** 292 | * Extracts an excerpt from text that matches the first instance of a phrase. 293 | * 294 | * @param string $text 295 | * @param string $phrase 296 | * @param array $options 297 | * @return string|null 298 | */ 299 | public static function excerpt($text, $phrase = '', $options = []) 300 | { 301 | $radius = $options['radius'] ?? 100; 302 | $omission = $options['omission'] ?? '...'; 303 | 304 | preg_match('/^(.*?)('.preg_quote((string) $phrase).')(.*)$/iu', (string) $text, $matches); 305 | 306 | if (empty($matches)) { 307 | return null; 308 | } 309 | 310 | $start = ltrim($matches[1]); 311 | 312 | $start = str(mb_substr($start, max(mb_strlen($start, 'UTF-8') - $radius, 0), $radius, 'UTF-8'))->ltrim()->unless( 313 | fn ($startWithRadius) => $startWithRadius->exactly($start), 314 | fn ($startWithRadius) => $startWithRadius->prepend($omission), 315 | ); 316 | 317 | $end = rtrim($matches[3]); 318 | 319 | $end = str(mb_substr($end, 0, $radius, 'UTF-8'))->rtrim()->unless( 320 | fn ($endWithRadius) => $endWithRadius->exactly($end), 321 | fn ($endWithRadius) => $endWithRadius->append($omission), 322 | ); 323 | 324 | return $start->append($matches[2], $end)->toString(); 325 | } 326 | 327 | /** 328 | * Cap a string with a single instance of a given value. 329 | * 330 | * @param string $value 331 | * @param string $cap 332 | * @return string 333 | */ 334 | public static function finish($value, $cap) 335 | { 336 | $quoted = preg_quote($cap, '/'); 337 | 338 | return preg_replace('/(?:'.$quoted.')+$/u', '', $value).$cap; 339 | } 340 | 341 | /** 342 | * Wrap the string with the given strings. 343 | * 344 | * @param string $value 345 | * @param string $before 346 | * @param string|null $after 347 | * @return string 348 | */ 349 | public static function wrap($value, $before, $after = null) 350 | { 351 | return $before.$value.($after ??= $before); 352 | } 353 | 354 | /** 355 | * Determine if a given string matches a given pattern. 356 | * 357 | * @param string|iterable $pattern 358 | * @param string $value 359 | * @return bool 360 | */ 361 | public static function is($pattern, $value) 362 | { 363 | $value = (string) $value; 364 | 365 | if (! is_iterable($pattern)) { 366 | $pattern = [$pattern]; 367 | } 368 | 369 | foreach ($pattern as $pattern) { 370 | $pattern = (string) $pattern; 371 | 372 | // If the given value is an exact match we can of course return true right 373 | // from the beginning. Otherwise, we will translate asterisks and do an 374 | // actual pattern match against the two strings to see if they match. 375 | if ($pattern === $value) { 376 | return true; 377 | } 378 | 379 | $pattern = preg_quote($pattern, '#'); 380 | 381 | // Asterisks are translated into zero-or-more regular expression wildcards 382 | // to make it convenient to check if the strings starts with the given 383 | // pattern such as "library/*", making any string check convenient. 384 | $pattern = str_replace('\*', '.*', $pattern); 385 | 386 | if (preg_match('#^'.$pattern.'\z#u', $value) === 1) { 387 | return true; 388 | } 389 | } 390 | 391 | return false; 392 | } 393 | 394 | /** 395 | * Determine if a given string is 7 bit ASCII. 396 | * 397 | * @param string $value 398 | * @return bool 399 | */ 400 | public static function isAscii($value) 401 | { 402 | return ASCII::is_ascii((string) $value); 403 | } 404 | 405 | /** 406 | * Determine if a given string is valid JSON. 407 | * 408 | * @param string $value 409 | * @return bool 410 | */ 411 | public static function isJson($value) 412 | { 413 | if (! is_string($value)) { 414 | return false; 415 | } 416 | 417 | try { 418 | json_decode($value, true, 512, JSON_THROW_ON_ERROR); 419 | } catch (JsonException) { 420 | return false; 421 | } 422 | 423 | return true; 424 | } 425 | 426 | /** 427 | * Determine if a given string is a valid UUID. 428 | * 429 | * @param string $value 430 | * @return bool 431 | */ 432 | public static function isUuid($value) 433 | { 434 | if (! is_string($value)) { 435 | return false; 436 | } 437 | 438 | return preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $value) > 0; 439 | } 440 | 441 | /** 442 | * Determine if a given string is a valid ULID. 443 | * 444 | * @param string $value 445 | * @return bool 446 | */ 447 | public static function isUlid($value) 448 | { 449 | if (! is_string($value)) { 450 | return false; 451 | } 452 | 453 | return Ulid::isValid($value); 454 | } 455 | 456 | /** 457 | * Convert a string to kebab case. 458 | * 459 | * @param string $value 460 | * @return string 461 | */ 462 | public static function kebab($value) 463 | { 464 | return static::snake($value, '-'); 465 | } 466 | 467 | /** 468 | * Return the length of the given string. 469 | * 470 | * @param string $value 471 | * @param string|null $encoding 472 | * @return int 473 | */ 474 | public static function length($value, $encoding = null) 475 | { 476 | if ($encoding) { 477 | return mb_strlen($value, $encoding); 478 | } 479 | 480 | return mb_strlen($value); 481 | } 482 | 483 | /** 484 | * Limit the number of characters in a string. 485 | * 486 | * @param string $value 487 | * @param int $limit 488 | * @param string $end 489 | * @return string 490 | */ 491 | public static function limit($value, $limit = 100, $end = '...') 492 | { 493 | if (mb_strwidth($value, 'UTF-8') <= $limit) { 494 | return $value; 495 | } 496 | 497 | return rtrim(mb_strimwidth($value, 0, $limit, '', 'UTF-8')).$end; 498 | } 499 | 500 | /** 501 | * Convert the given string to lower-case. 502 | * 503 | * @param string $value 504 | * @return string 505 | */ 506 | public static function lower($value) 507 | { 508 | return mb_strtolower($value, 'UTF-8'); 509 | } 510 | 511 | /** 512 | * Limit the number of words in a string. 513 | * 514 | * @param string $value 515 | * @param int $words 516 | * @param string $end 517 | * @return string 518 | */ 519 | public static function words($value, $words = 100, $end = '...') 520 | { 521 | preg_match('/^\s*+(?:\S++\s*+){1,'.$words.'}/u', $value, $matches); 522 | 523 | if (! isset($matches[0]) || static::length($value) === static::length($matches[0])) { 524 | return $value; 525 | } 526 | 527 | return rtrim($matches[0]).$end; 528 | } 529 | 530 | /** 531 | * Converts GitHub flavored Markdown into HTML. 532 | * 533 | * @param string $string 534 | * @param array $options 535 | * @return string 536 | */ 537 | public static function markdown($string, array $options = []) 538 | { 539 | $converter = new GithubFlavoredMarkdownConverter($options); 540 | 541 | return (string) $converter->convert($string); 542 | } 543 | 544 | /** 545 | * Converts inline Markdown into HTML. 546 | * 547 | * @param string $string 548 | * @param array $options 549 | * @return string 550 | */ 551 | public static function inlineMarkdown($string, array $options = []) 552 | { 553 | $environment = new Environment($options); 554 | 555 | $environment->addExtension(new GithubFlavoredMarkdownExtension()); 556 | $environment->addExtension(new InlinesOnlyExtension()); 557 | 558 | $converter = new MarkdownConverter($environment); 559 | 560 | return (string) $converter->convert($string); 561 | } 562 | 563 | /** 564 | * Masks a portion of a string with a repeated character. 565 | * 566 | * @param string $string 567 | * @param string $character 568 | * @param int $index 569 | * @param int|null $length 570 | * @param string $encoding 571 | * @return string 572 | */ 573 | public static function mask($string, $character, $index, $length = null, $encoding = 'UTF-8') 574 | { 575 | if ($character === '') { 576 | return $string; 577 | } 578 | 579 | $segment = mb_substr($string, $index, $length, $encoding); 580 | 581 | if ($segment === '') { 582 | return $string; 583 | } 584 | 585 | $strlen = mb_strlen($string, $encoding); 586 | $startIndex = $index; 587 | 588 | if ($index < 0) { 589 | $startIndex = $index < -$strlen ? 0 : $strlen + $index; 590 | } 591 | 592 | $start = mb_substr($string, 0, $startIndex, $encoding); 593 | $segmentLen = mb_strlen($segment, $encoding); 594 | $end = mb_substr($string, $startIndex + $segmentLen); 595 | 596 | return $start.str_repeat(mb_substr($character, 0, 1, $encoding), $segmentLen).$end; 597 | } 598 | 599 | /** 600 | * Get the string matching the given pattern. 601 | * 602 | * @param string $pattern 603 | * @param string $subject 604 | * @return string 605 | */ 606 | public static function match($pattern, $subject) 607 | { 608 | preg_match($pattern, $subject, $matches); 609 | 610 | if (! $matches) { 611 | return ''; 612 | } 613 | 614 | return $matches[1] ?? $matches[0]; 615 | } 616 | 617 | /** 618 | * Get the string matching the given pattern. 619 | * 620 | * @param string $pattern 621 | * @param string $subject 622 | * @return \QL\Collect\Support\Collection 623 | */ 624 | public static function matchAll($pattern, $subject) 625 | { 626 | preg_match_all($pattern, $subject, $matches); 627 | 628 | if (empty($matches[0])) { 629 | return collect(); 630 | } 631 | 632 | return collect($matches[1] ?? $matches[0]); 633 | } 634 | 635 | /** 636 | * Pad both sides of a string with another. 637 | * 638 | * @param string $value 639 | * @param int $length 640 | * @param string $pad 641 | * @return string 642 | */ 643 | public static function padBoth($value, $length, $pad = ' ') 644 | { 645 | $short = max(0, $length - mb_strlen($value)); 646 | $shortLeft = floor($short / 2); 647 | $shortRight = ceil($short / 2); 648 | 649 | return mb_substr(str_repeat($pad, $shortLeft), 0, $shortLeft). 650 | $value. 651 | mb_substr(str_repeat($pad, $shortRight), 0, $shortRight); 652 | } 653 | 654 | /** 655 | * Pad the left side of a string with another. 656 | * 657 | * @param string $value 658 | * @param int $length 659 | * @param string $pad 660 | * @return string 661 | */ 662 | public static function padLeft($value, $length, $pad = ' ') 663 | { 664 | $short = max(0, $length - mb_strlen($value)); 665 | 666 | return mb_substr(str_repeat($pad, $short), 0, $short).$value; 667 | } 668 | 669 | /** 670 | * Pad the right side of a string with another. 671 | * 672 | * @param string $value 673 | * @param int $length 674 | * @param string $pad 675 | * @return string 676 | */ 677 | public static function padRight($value, $length, $pad = ' ') 678 | { 679 | $short = max(0, $length - mb_strlen($value)); 680 | 681 | return $value.mb_substr(str_repeat($pad, $short), 0, $short); 682 | } 683 | 684 | /** 685 | * Parse a Class[@]method style callback into class and method. 686 | * 687 | * @param string $callback 688 | * @param string|null $default 689 | * @return array 690 | */ 691 | public static function parseCallback($callback, $default = null) 692 | { 693 | return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default]; 694 | } 695 | 696 | /** 697 | * Get the plural form of an English word. 698 | * 699 | * @param string $value 700 | * @param int|array|\Countable $count 701 | * @return string 702 | */ 703 | public static function plural($value, $count = 2) 704 | { 705 | return Pluralizer::plural($value, $count); 706 | } 707 | 708 | /** 709 | * Pluralize the last word of an English, studly caps case string. 710 | * 711 | * @param string $value 712 | * @param int|array|\Countable $count 713 | * @return string 714 | */ 715 | public static function pluralStudly($value, $count = 2) 716 | { 717 | $parts = preg_split('/(.)(?=[A-Z])/u', $value, -1, PREG_SPLIT_DELIM_CAPTURE); 718 | 719 | $lastWord = array_pop($parts); 720 | 721 | return implode('', $parts).self::plural($lastWord, $count); 722 | } 723 | 724 | /** 725 | * Generate a more truly "random" alpha-numeric string. 726 | * 727 | * @param int $length 728 | * @return string 729 | */ 730 | public static function random($length = 16) 731 | { 732 | return (static::$randomStringFactory ?? function ($length) { 733 | $string = ''; 734 | 735 | while (($len = strlen($string)) < $length) { 736 | $size = $length - $len; 737 | 738 | $bytesSize = (int) ceil($size / 3) * 3; 739 | 740 | $bytes = random_bytes($bytesSize); 741 | 742 | $string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size); 743 | } 744 | 745 | return $string; 746 | })($length); 747 | } 748 | 749 | /** 750 | * Set the callable that will be used to generate random strings. 751 | * 752 | * @param callable|null $factory 753 | * @return void 754 | */ 755 | public static function createRandomStringsUsing(callable $factory = null) 756 | { 757 | static::$randomStringFactory = $factory; 758 | } 759 | 760 | /** 761 | * Set the sequence that will be used to generate random strings. 762 | * 763 | * @param array $sequence 764 | * @param callable|null $whenMissing 765 | * @return void 766 | */ 767 | public static function createRandomStringsUsingSequence(array $sequence, $whenMissing = null) 768 | { 769 | $next = 0; 770 | 771 | $whenMissing ??= function ($length) use (&$next) { 772 | $factoryCache = static::$randomStringFactory; 773 | 774 | static::$randomStringFactory = null; 775 | 776 | $randomString = static::random($length); 777 | 778 | static::$randomStringFactory = $factoryCache; 779 | 780 | $next++; 781 | 782 | return $randomString; 783 | }; 784 | 785 | static::createRandomStringsUsing(function ($length) use (&$next, $sequence, $whenMissing) { 786 | if (array_key_exists($next, $sequence)) { 787 | return $sequence[$next++]; 788 | } 789 | 790 | return $whenMissing($length); 791 | }); 792 | } 793 | 794 | /** 795 | * Indicate that random strings should be created normally and not using a custom factory. 796 | * 797 | * @return void 798 | */ 799 | public static function createRandomStringsNormally() 800 | { 801 | static::$randomStringFactory = null; 802 | } 803 | 804 | /** 805 | * Repeat the given string. 806 | * 807 | * @param string $string 808 | * @param int $times 809 | * @return string 810 | */ 811 | public static function repeat(string $string, int $times) 812 | { 813 | return str_repeat($string, $times); 814 | } 815 | 816 | /** 817 | * Replace a given value in the string sequentially with an array. 818 | * 819 | * @param string $search 820 | * @param iterable $replace 821 | * @param string $subject 822 | * @return string 823 | */ 824 | public static function replaceArray($search, $replace, $subject) 825 | { 826 | if ($replace instanceof Traversable) { 827 | $replace = collect($replace)->all(); 828 | } 829 | 830 | $segments = explode($search, $subject); 831 | 832 | $result = array_shift($segments); 833 | 834 | foreach ($segments as $segment) { 835 | $result .= (array_shift($replace) ?? $search).$segment; 836 | } 837 | 838 | return $result; 839 | } 840 | 841 | /** 842 | * Replace the given value in the given string. 843 | * 844 | * @param string|iterable $search 845 | * @param string|iterable $replace 846 | * @param string|iterable $subject 847 | * @return string 848 | */ 849 | public static function replace($search, $replace, $subject) 850 | { 851 | if ($search instanceof Traversable) { 852 | $search = collect($search)->all(); 853 | } 854 | 855 | if ($replace instanceof Traversable) { 856 | $replace = collect($replace)->all(); 857 | } 858 | 859 | if ($subject instanceof Traversable) { 860 | $subject = collect($subject)->all(); 861 | } 862 | 863 | return str_replace($search, $replace, $subject); 864 | } 865 | 866 | /** 867 | * Replace the first occurrence of a given value in the string. 868 | * 869 | * @param string $search 870 | * @param string $replace 871 | * @param string $subject 872 | * @return string 873 | */ 874 | public static function replaceFirst($search, $replace, $subject) 875 | { 876 | $search = (string) $search; 877 | 878 | if ($search === '') { 879 | return $subject; 880 | } 881 | 882 | $position = strpos($subject, $search); 883 | 884 | if ($position !== false) { 885 | return substr_replace($subject, $replace, $position, strlen($search)); 886 | } 887 | 888 | return $subject; 889 | } 890 | 891 | /** 892 | * Replace the last occurrence of a given value in the string. 893 | * 894 | * @param string $search 895 | * @param string $replace 896 | * @param string $subject 897 | * @return string 898 | */ 899 | public static function replaceLast($search, $replace, $subject) 900 | { 901 | if ($search === '') { 902 | return $subject; 903 | } 904 | 905 | $position = strrpos($subject, $search); 906 | 907 | if ($position !== false) { 908 | return substr_replace($subject, $replace, $position, strlen($search)); 909 | } 910 | 911 | return $subject; 912 | } 913 | 914 | /** 915 | * Remove any occurrence of the given string in the subject. 916 | * 917 | * @param string|iterable $search 918 | * @param string $subject 919 | * @param bool $caseSensitive 920 | * @return string 921 | */ 922 | public static function remove($search, $subject, $caseSensitive = true) 923 | { 924 | if ($search instanceof Traversable) { 925 | $search = collect($search)->all(); 926 | } 927 | 928 | $subject = $caseSensitive 929 | ? str_replace($search, '', $subject) 930 | : str_ireplace($search, '', $subject); 931 | 932 | return $subject; 933 | } 934 | 935 | /** 936 | * Reverse the given string. 937 | * 938 | * @param string $value 939 | * @return string 940 | */ 941 | public static function reverse(string $value) 942 | { 943 | return implode(array_reverse(mb_str_split($value))); 944 | } 945 | 946 | /** 947 | * Begin a string with a single instance of a given value. 948 | * 949 | * @param string $value 950 | * @param string $prefix 951 | * @return string 952 | */ 953 | public static function start($value, $prefix) 954 | { 955 | $quoted = preg_quote($prefix, '/'); 956 | 957 | return $prefix.preg_replace('/^(?:'.$quoted.')+/u', '', $value); 958 | } 959 | 960 | /** 961 | * Convert the given string to upper-case. 962 | * 963 | * @param string $value 964 | * @return string 965 | */ 966 | public static function upper($value) 967 | { 968 | return mb_strtoupper($value, 'UTF-8'); 969 | } 970 | 971 | /** 972 | * Convert the given string to title case. 973 | * 974 | * @param string $value 975 | * @return string 976 | */ 977 | public static function title($value) 978 | { 979 | return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8'); 980 | } 981 | 982 | /** 983 | * Convert the given string to title case for each word. 984 | * 985 | * @param string $value 986 | * @return string 987 | */ 988 | public static function headline($value) 989 | { 990 | $parts = explode(' ', $value); 991 | 992 | $parts = count($parts) > 1 993 | ? array_map([static::class, 'title'], $parts) 994 | : array_map([static::class, 'title'], static::ucsplit(implode('_', $parts))); 995 | 996 | $collapsed = static::replace(['-', '_', ' '], '_', implode('_', $parts)); 997 | 998 | return implode(' ', array_filter(explode('_', $collapsed))); 999 | } 1000 | 1001 | /** 1002 | * Get the singular form of an English word. 1003 | * 1004 | * @param string $value 1005 | * @return string 1006 | */ 1007 | public static function singular($value) 1008 | { 1009 | return Pluralizer::singular($value); 1010 | } 1011 | 1012 | /** 1013 | * Generate a URL friendly "slug" from a given string. 1014 | * 1015 | * @param string $title 1016 | * @param string $separator 1017 | * @param string|null $language 1018 | * @param array $dictionary 1019 | * @return string 1020 | */ 1021 | public static function slug($title, $separator = '-', $language = 'en', $dictionary = ['@' => 'at']) 1022 | { 1023 | $title = $language ? static::ascii($title, $language) : $title; 1024 | 1025 | // Convert all dashes/underscores into separator 1026 | $flip = $separator === '-' ? '_' : '-'; 1027 | 1028 | $title = preg_replace('!['.preg_quote($flip).']+!u', $separator, $title); 1029 | 1030 | // Replace dictionary words 1031 | foreach ($dictionary as $key => $value) { 1032 | $dictionary[$key] = $separator.$value.$separator; 1033 | } 1034 | 1035 | $title = str_replace(array_keys($dictionary), array_values($dictionary), $title); 1036 | 1037 | // Remove all characters that are not the separator, letters, numbers, or whitespace 1038 | $title = preg_replace('![^'.preg_quote($separator).'\pL\pN\s]+!u', '', static::lower($title)); 1039 | 1040 | // Replace all separator characters and whitespace by a single separator 1041 | $title = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $title); 1042 | 1043 | return trim($title, $separator); 1044 | } 1045 | 1046 | /** 1047 | * Convert a string to snake case. 1048 | * 1049 | * @param string $value 1050 | * @param string $delimiter 1051 | * @return string 1052 | */ 1053 | public static function snake($value, $delimiter = '_') 1054 | { 1055 | $key = $value; 1056 | 1057 | if (isset(static::$snakeCache[$key][$delimiter])) { 1058 | return static::$snakeCache[$key][$delimiter]; 1059 | } 1060 | 1061 | if (! ctype_lower($value)) { 1062 | $value = preg_replace('/\s+/u', '', ucwords($value)); 1063 | 1064 | $value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $value)); 1065 | } 1066 | 1067 | return static::$snakeCache[$key][$delimiter] = $value; 1068 | } 1069 | 1070 | /** 1071 | * Remove all "extra" blank space from the given string. 1072 | * 1073 | * @param string $value 1074 | * @return string 1075 | */ 1076 | public static function squish($value) 1077 | { 1078 | return preg_replace('~(\s|\x{3164})+~u', ' ', preg_replace('~^[\s\x{FEFF}]+|[\s\x{FEFF}]+$~u', '', $value)); 1079 | } 1080 | 1081 | /** 1082 | * Determine if a given string starts with a given substring. 1083 | * 1084 | * @param string $haystack 1085 | * @param string|iterable $needles 1086 | * @return bool 1087 | */ 1088 | public static function startsWith($haystack, $needles) 1089 | { 1090 | if (! is_iterable($needles)) { 1091 | $needles = [$needles]; 1092 | } 1093 | 1094 | foreach ($needles as $needle) { 1095 | if ((string) $needle !== '' && str_starts_with($haystack, $needle)) { 1096 | return true; 1097 | } 1098 | } 1099 | 1100 | return false; 1101 | } 1102 | 1103 | /** 1104 | * Convert a value to studly caps case. 1105 | * 1106 | * @param string $value 1107 | * @return string 1108 | */ 1109 | public static function studly($value) 1110 | { 1111 | $key = $value; 1112 | 1113 | if (isset(static::$studlyCache[$key])) { 1114 | return static::$studlyCache[$key]; 1115 | } 1116 | 1117 | $words = explode(' ', static::replace(['-', '_'], ' ', $value)); 1118 | 1119 | $studlyWords = array_map(fn ($word) => static::ucfirst($word), $words); 1120 | 1121 | return static::$studlyCache[$key] = implode($studlyWords); 1122 | } 1123 | 1124 | /** 1125 | * Returns the portion of the string specified by the start and length parameters. 1126 | * 1127 | * @param string $string 1128 | * @param int $start 1129 | * @param int|null $length 1130 | * @param string $encoding 1131 | * @return string 1132 | */ 1133 | public static function substr($string, $start, $length = null, $encoding = 'UTF-8') 1134 | { 1135 | return mb_substr($string, $start, $length, $encoding); 1136 | } 1137 | 1138 | /** 1139 | * Returns the number of substring occurrences. 1140 | * 1141 | * @param string $haystack 1142 | * @param string $needle 1143 | * @param int $offset 1144 | * @param int|null $length 1145 | * @return int 1146 | */ 1147 | public static function substrCount($haystack, $needle, $offset = 0, $length = null) 1148 | { 1149 | if (! is_null($length)) { 1150 | return substr_count($haystack, $needle, $offset, $length); 1151 | } 1152 | 1153 | return substr_count($haystack, $needle, $offset); 1154 | } 1155 | 1156 | /** 1157 | * Replace text within a portion of a string. 1158 | * 1159 | * @param string|string[] $string 1160 | * @param string|string[] $replace 1161 | * @param int|int[] $offset 1162 | * @param int|int[]|null $length 1163 | * @return string|string[] 1164 | */ 1165 | public static function substrReplace($string, $replace, $offset = 0, $length = null) 1166 | { 1167 | if ($length === null) { 1168 | $length = strlen($string); 1169 | } 1170 | 1171 | return substr_replace($string, $replace, $offset, $length); 1172 | } 1173 | 1174 | /** 1175 | * Swap multiple keywords in a string with other keywords. 1176 | * 1177 | * @param array $map 1178 | * @param string $subject 1179 | * @return string 1180 | */ 1181 | public static function swap(array $map, $subject) 1182 | { 1183 | return strtr($subject, $map); 1184 | } 1185 | 1186 | /** 1187 | * Make a string's first character lowercase. 1188 | * 1189 | * @param string $string 1190 | * @return string 1191 | */ 1192 | public static function lcfirst($string) 1193 | { 1194 | return static::lower(static::substr($string, 0, 1)).static::substr($string, 1); 1195 | } 1196 | 1197 | /** 1198 | * Make a string's first character uppercase. 1199 | * 1200 | * @param string $string 1201 | * @return string 1202 | */ 1203 | public static function ucfirst($string) 1204 | { 1205 | return static::upper(static::substr($string, 0, 1)).static::substr($string, 1); 1206 | } 1207 | 1208 | /** 1209 | * Split a string into pieces by uppercase characters. 1210 | * 1211 | * @param string $string 1212 | * @return string[] 1213 | */ 1214 | public static function ucsplit($string) 1215 | { 1216 | return preg_split('/(?=\p{Lu})/u', $string, -1, PREG_SPLIT_NO_EMPTY); 1217 | } 1218 | 1219 | /** 1220 | * Get the number of words a string contains. 1221 | * 1222 | * @param string $string 1223 | * @param string|null $characters 1224 | * @return int 1225 | */ 1226 | public static function wordCount($string, $characters = null) 1227 | { 1228 | return str_word_count($string, 0, $characters); 1229 | } 1230 | 1231 | /** 1232 | * Generate a UUID (version 4). 1233 | * 1234 | * @return \Ramsey\Uuid\UuidInterface 1235 | */ 1236 | public static function uuid() 1237 | { 1238 | return static::$uuidFactory 1239 | ? call_user_func(static::$uuidFactory) 1240 | : Uuid::uuid4(); 1241 | } 1242 | 1243 | /** 1244 | * Generate a time-ordered UUID (version 4). 1245 | * 1246 | * @return \Ramsey\Uuid\UuidInterface 1247 | */ 1248 | public static function orderedUuid() 1249 | { 1250 | if (static::$uuidFactory) { 1251 | return call_user_func(static::$uuidFactory); 1252 | } 1253 | 1254 | $factory = new UuidFactory; 1255 | 1256 | $factory->setRandomGenerator(new CombGenerator( 1257 | $factory->getRandomGenerator(), 1258 | $factory->getNumberConverter() 1259 | )); 1260 | 1261 | $factory->setCodec(new TimestampFirstCombCodec( 1262 | $factory->getUuidBuilder() 1263 | )); 1264 | 1265 | return $factory->uuid4(); 1266 | } 1267 | 1268 | /** 1269 | * Set the callable that will be used to generate UUIDs. 1270 | * 1271 | * @param callable|null $factory 1272 | * @return void 1273 | */ 1274 | public static function createUuidsUsing(callable $factory = null) 1275 | { 1276 | static::$uuidFactory = $factory; 1277 | } 1278 | 1279 | /** 1280 | * Set the sequence that will be used to generate UUIDs. 1281 | * 1282 | * @param array $sequence 1283 | * @param callable|null $whenMissing 1284 | * @return void 1285 | */ 1286 | public static function createUuidsUsingSequence(array $sequence, $whenMissing = null) 1287 | { 1288 | $next = 0; 1289 | 1290 | $whenMissing ??= function () use (&$next) { 1291 | $factoryCache = static::$uuidFactory; 1292 | 1293 | static::$uuidFactory = null; 1294 | 1295 | $uuid = static::uuid(); 1296 | 1297 | static::$uuidFactory = $factoryCache; 1298 | 1299 | $next++; 1300 | 1301 | return $uuid; 1302 | }; 1303 | 1304 | static::createUuidsUsing(function () use (&$next, $sequence, $whenMissing) { 1305 | if (array_key_exists($next, $sequence)) { 1306 | return $sequence[$next++]; 1307 | } 1308 | 1309 | return $whenMissing(); 1310 | }); 1311 | } 1312 | 1313 | /** 1314 | * Always return the same UUID when generating new UUIDs. 1315 | * 1316 | * @param \Closure|null $callback 1317 | * @return \Ramsey\Uuid\UuidInterface 1318 | */ 1319 | public static function freezeUuids(Closure $callback = null) 1320 | { 1321 | $uuid = Str::uuid(); 1322 | 1323 | Str::createUuidsUsing(fn () => $uuid); 1324 | 1325 | if ($callback !== null) { 1326 | try { 1327 | $callback($uuid); 1328 | } finally { 1329 | Str::createUuidsNormally(); 1330 | } 1331 | } 1332 | 1333 | return $uuid; 1334 | } 1335 | 1336 | /** 1337 | * Indicate that UUIDs should be created normally and not using a custom factory. 1338 | * 1339 | * @return void 1340 | */ 1341 | public static function createUuidsNormally() 1342 | { 1343 | static::$uuidFactory = null; 1344 | } 1345 | 1346 | /** 1347 | * Generate a ULID. 1348 | * 1349 | * @return \Symfony\Component\Uid\Ulid 1350 | */ 1351 | public static function ulid() 1352 | { 1353 | return new Ulid(); 1354 | } 1355 | 1356 | /** 1357 | * Remove all strings from the casing caches. 1358 | * 1359 | * @return void 1360 | */ 1361 | public static function flushCache() 1362 | { 1363 | static::$snakeCache = []; 1364 | static::$camelCache = []; 1365 | static::$studlyCache = []; 1366 | } 1367 | } 1368 | -------------------------------------------------------------------------------- /src/Collect/Support/Traits/Conditionable.php: -------------------------------------------------------------------------------- 1 | condition($value); 31 | } 32 | 33 | if ($value) { 34 | return $callback($this, $value) ?? $this; 35 | } elseif ($default) { 36 | return $default($this, $value) ?? $this; 37 | } 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * Apply the callback if the given "value" is (or resolves to) falsy. 44 | * 45 | * @template TUnlessParameter 46 | * @template TUnlessReturnType 47 | * 48 | * @param (\Closure($this): TUnlessParameter)|TUnlessParameter|null $value 49 | * @param (callable($this, TUnlessParameter): TUnlessReturnType)|null $callback 50 | * @param (callable($this, TUnlessParameter): TUnlessReturnType)|null $default 51 | * @return $this|TUnlessReturnType 52 | */ 53 | public function unless($value = null, callable $callback = null, callable $default = null) 54 | { 55 | $value = $value instanceof Closure ? $value($this) : $value; 56 | 57 | if (func_num_args() === 0) { 58 | return (new HigherOrderWhenProxy($this))->negateConditionOnCapture(); 59 | } 60 | 61 | if (func_num_args() === 1) { 62 | return (new HigherOrderWhenProxy($this))->condition(! $value); 63 | } 64 | 65 | if (! $value) { 66 | return $callback($this, $value) ?? $this; 67 | } elseif ($default) { 68 | return $default($this, $value) ?? $this; 69 | } 70 | 71 | return $this; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Collect/Support/Traits/EnumeratesValues.php: -------------------------------------------------------------------------------- 1 | 68 | */ 69 | protected static $proxies = [ 70 | 'average', 71 | 'avg', 72 | 'contains', 73 | 'doesntContain', 74 | 'each', 75 | 'every', 76 | 'filter', 77 | 'first', 78 | 'flatMap', 79 | 'groupBy', 80 | 'keyBy', 81 | 'map', 82 | 'max', 83 | 'min', 84 | 'partition', 85 | 'reject', 86 | 'skipUntil', 87 | 'skipWhile', 88 | 'some', 89 | 'sortBy', 90 | 'sortByDesc', 91 | 'sum', 92 | 'takeUntil', 93 | 'takeWhile', 94 | 'unique', 95 | 'unless', 96 | 'until', 97 | 'when', 98 | ]; 99 | 100 | /** 101 | * Create a new collection instance if the value isn't one already. 102 | * 103 | * @template TMakeKey of array-key 104 | * @template TMakeValue 105 | * 106 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable|null $items 107 | * @return static 108 | */ 109 | public static function make($items = []) 110 | { 111 | return new static($items); 112 | } 113 | 114 | /** 115 | * Wrap the given value in a collection if applicable. 116 | * 117 | * @template TWrapValue 118 | * 119 | * @param iterable|TWrapValue $value 120 | * @return static 121 | */ 122 | public static function wrap($value) 123 | { 124 | return $value instanceof Enumerable 125 | ? new static($value) 126 | : new static(Arr::wrap($value)); 127 | } 128 | 129 | /** 130 | * Get the underlying items from the given collection if applicable. 131 | * 132 | * @template TUnwrapKey of array-key 133 | * @template TUnwrapValue 134 | * 135 | * @param array|static $value 136 | * @return array 137 | */ 138 | public static function unwrap($value) 139 | { 140 | return $value instanceof Enumerable ? $value->all() : $value; 141 | } 142 | 143 | /** 144 | * Create a new instance with no items. 145 | * 146 | * @return static 147 | */ 148 | public static function empty() 149 | { 150 | return new static([]); 151 | } 152 | 153 | /** 154 | * Create a new collection by invoking the callback a given amount of times. 155 | * 156 | * @template TTimesValue 157 | * 158 | * @param int $number 159 | * @param (callable(int): TTimesValue)|null $callback 160 | * @return static 161 | */ 162 | public static function times($number, callable $callback = null) 163 | { 164 | if ($number < 1) { 165 | return new static; 166 | } 167 | 168 | return static::range(1, $number) 169 | ->unless($callback == null) 170 | ->map($callback); 171 | } 172 | 173 | /** 174 | * Alias for the "avg" method. 175 | * 176 | * @param (callable(TValue): float|int)|string|null $callback 177 | * @return float|int|null 178 | */ 179 | public function average($callback = null) 180 | { 181 | return $this->avg($callback); 182 | } 183 | 184 | /** 185 | * Alias for the "contains" method. 186 | * 187 | * @param (callable(TValue, TKey): bool)|TValue|string $key 188 | * @param mixed $operator 189 | * @param mixed $value 190 | * @return bool 191 | */ 192 | public function some($key, $operator = null, $value = null) 193 | { 194 | return $this->contains(...func_get_args()); 195 | } 196 | 197 | /** 198 | * Dump the items and end the script. 199 | * 200 | * @param mixed ...$args 201 | * @return never 202 | */ 203 | public function dd(...$args) 204 | { 205 | $this->dump(...$args); 206 | 207 | exit(1); 208 | } 209 | 210 | /** 211 | * Dump the items. 212 | * 213 | * @return $this 214 | */ 215 | public function dump() 216 | { 217 | (new Collection(func_get_args())) 218 | ->push($this->all()) 219 | ->each(function ($item) { 220 | VarDumper::dump($item); 221 | }); 222 | 223 | return $this; 224 | } 225 | 226 | /** 227 | * Execute a callback over each item. 228 | * 229 | * @param callable(TValue, TKey): mixed $callback 230 | * @return $this 231 | */ 232 | public function each(callable $callback) 233 | { 234 | foreach ($this as $key => $item) { 235 | if ($callback($item, $key) === false) { 236 | break; 237 | } 238 | } 239 | 240 | return $this; 241 | } 242 | 243 | /** 244 | * Execute a callback over each nested chunk of items. 245 | * 246 | * @param callable(...mixed): mixed $callback 247 | * @return static 248 | */ 249 | public function eachSpread(callable $callback) 250 | { 251 | return $this->each(function ($chunk, $key) use ($callback) { 252 | $chunk[] = $key; 253 | 254 | return $callback(...$chunk); 255 | }); 256 | } 257 | 258 | /** 259 | * Determine if all items pass the given truth test. 260 | * 261 | * @param (callable(TValue, TKey): bool)|TValue|string $key 262 | * @param mixed $operator 263 | * @param mixed $value 264 | * @return bool 265 | */ 266 | public function every($key, $operator = null, $value = null) 267 | { 268 | if (func_num_args() === 1) { 269 | $callback = $this->valueRetriever($key); 270 | 271 | foreach ($this as $k => $v) { 272 | if (! $callback($v, $k)) { 273 | return false; 274 | } 275 | } 276 | 277 | return true; 278 | } 279 | 280 | return $this->every($this->operatorForWhere(...func_get_args())); 281 | } 282 | 283 | /** 284 | * Get the first item by the given key value pair. 285 | * 286 | * @param callable|string $key 287 | * @param mixed $operator 288 | * @param mixed $value 289 | * @return TValue|null 290 | */ 291 | public function firstWhere($key, $operator = null, $value = null) 292 | { 293 | return $this->first($this->operatorForWhere(...func_get_args())); 294 | } 295 | 296 | /** 297 | * Get a single key's value from the first matching item in the collection. 298 | * 299 | * @param string $key 300 | * @param mixed $default 301 | * @return mixed 302 | */ 303 | public function value($key, $default = null) 304 | { 305 | if ($value = $this->firstWhere($key)) { 306 | return data_get($value, $key, $default); 307 | } 308 | 309 | return value($default); 310 | } 311 | 312 | /** 313 | * Determine if the collection is not empty. 314 | * 315 | * @return bool 316 | */ 317 | public function isNotEmpty() 318 | { 319 | return ! $this->isEmpty(); 320 | } 321 | 322 | /** 323 | * Run a map over each nested chunk of items. 324 | * 325 | * @template TMapSpreadValue 326 | * 327 | * @param callable(mixed): TMapSpreadValue $callback 328 | * @return static 329 | */ 330 | public function mapSpread(callable $callback) 331 | { 332 | return $this->map(function ($chunk, $key) use ($callback) { 333 | $chunk[] = $key; 334 | 335 | return $callback(...$chunk); 336 | }); 337 | } 338 | 339 | /** 340 | * Run a grouping map over the items. 341 | * 342 | * The callback should return an associative array with a single key/value pair. 343 | * 344 | * @template TMapToGroupsKey of array-key 345 | * @template TMapToGroupsValue 346 | * 347 | * @param callable(TValue, TKey): array $callback 348 | * @return static> 349 | */ 350 | public function mapToGroups(callable $callback) 351 | { 352 | $groups = $this->mapToDictionary($callback); 353 | 354 | return $groups->map([$this, 'make']); 355 | } 356 | 357 | /** 358 | * Map a collection and flatten the result by a single level. 359 | * 360 | * @template TFlatMapKey of array-key 361 | * @template TFlatMapValue 362 | * 363 | * @param callable(TValue, TKey): (\QL\Collect\Support\Collection|array) $callback 364 | * @return static 365 | */ 366 | public function flatMap(callable $callback) 367 | { 368 | return $this->map($callback)->collapse(); 369 | } 370 | 371 | /** 372 | * Map the values into a new class. 373 | * 374 | * @template TMapIntoValue 375 | * 376 | * @param class-string $class 377 | * @return static 378 | */ 379 | public function mapInto($class) 380 | { 381 | return $this->map(fn ($value, $key) => new $class($value, $key)); 382 | } 383 | 384 | /** 385 | * Get the min value of a given key. 386 | * 387 | * @param (callable(TValue):mixed)|string|null $callback 388 | * @return mixed 389 | */ 390 | public function min($callback = null) 391 | { 392 | $callback = $this->valueRetriever($callback); 393 | 394 | return $this->map(fn ($value) => $callback($value)) 395 | ->filter(fn ($value) => ! is_null($value)) 396 | ->reduce(fn ($result, $value) => is_null($result) || $value < $result ? $value : $result); 397 | } 398 | 399 | /** 400 | * Get the max value of a given key. 401 | * 402 | * @param (callable(TValue):mixed)|string|null $callback 403 | * @return mixed 404 | */ 405 | public function max($callback = null) 406 | { 407 | $callback = $this->valueRetriever($callback); 408 | 409 | return $this->filter(fn ($value) => ! is_null($value))->reduce(function ($result, $item) use ($callback) { 410 | $value = $callback($item); 411 | 412 | return is_null($result) || $value > $result ? $value : $result; 413 | }); 414 | } 415 | 416 | /** 417 | * "Paginate" the collection by slicing it into a smaller collection. 418 | * 419 | * @param int $page 420 | * @param int $perPage 421 | * @return static 422 | */ 423 | public function forPage($page, $perPage) 424 | { 425 | $offset = max(0, ($page - 1) * $perPage); 426 | 427 | return $this->slice($offset, $perPage); 428 | } 429 | 430 | /** 431 | * Partition the collection into two arrays using the given callback or key. 432 | * 433 | * @param (callable(TValue, TKey): bool)|TValue|string $key 434 | * @param TValue|string|null $operator 435 | * @param TValue|null $value 436 | * @return static, static> 437 | */ 438 | public function partition($key, $operator = null, $value = null) 439 | { 440 | $passed = []; 441 | $failed = []; 442 | 443 | $callback = func_num_args() === 1 444 | ? $this->valueRetriever($key) 445 | : $this->operatorForWhere(...func_get_args()); 446 | 447 | foreach ($this as $key => $item) { 448 | if ($callback($item, $key)) { 449 | $passed[$key] = $item; 450 | } else { 451 | $failed[$key] = $item; 452 | } 453 | } 454 | 455 | return new static([new static($passed), new static($failed)]); 456 | } 457 | 458 | /** 459 | * Get the sum of the given values. 460 | * 461 | * @param (callable(TValue): mixed)|string|null $callback 462 | * @return mixed 463 | */ 464 | public function sum($callback = null) 465 | { 466 | $callback = is_null($callback) 467 | ? $this->identity() 468 | : $this->valueRetriever($callback); 469 | 470 | return $this->reduce(fn ($result, $item) => $result + $callback($item), 0); 471 | } 472 | 473 | /** 474 | * Apply the callback if the collection is empty. 475 | * 476 | * @template TWhenEmptyReturnType 477 | * 478 | * @param (callable($this): TWhenEmptyReturnType) $callback 479 | * @param (callable($this): TWhenEmptyReturnType)|null $default 480 | * @return $this|TWhenEmptyReturnType 481 | */ 482 | public function whenEmpty(callable $callback, callable $default = null) 483 | { 484 | return $this->when($this->isEmpty(), $callback, $default); 485 | } 486 | 487 | /** 488 | * Apply the callback if the collection is not empty. 489 | * 490 | * @template TWhenNotEmptyReturnType 491 | * 492 | * @param callable($this): TWhenNotEmptyReturnType $callback 493 | * @param (callable($this): TWhenNotEmptyReturnType)|null $default 494 | * @return $this|TWhenNotEmptyReturnType 495 | */ 496 | public function whenNotEmpty(callable $callback, callable $default = null) 497 | { 498 | return $this->when($this->isNotEmpty(), $callback, $default); 499 | } 500 | 501 | /** 502 | * Apply the callback unless the collection is empty. 503 | * 504 | * @template TUnlessEmptyReturnType 505 | * 506 | * @param callable($this): TUnlessEmptyReturnType $callback 507 | * @param (callable($this): TUnlessEmptyReturnType)|null $default 508 | * @return $this|TUnlessEmptyReturnType 509 | */ 510 | public function unlessEmpty(callable $callback, callable $default = null) 511 | { 512 | return $this->whenNotEmpty($callback, $default); 513 | } 514 | 515 | /** 516 | * Apply the callback unless the collection is not empty. 517 | * 518 | * @template TUnlessNotEmptyReturnType 519 | * 520 | * @param callable($this): TUnlessNotEmptyReturnType $callback 521 | * @param (callable($this): TUnlessNotEmptyReturnType)|null $default 522 | * @return $this|TUnlessNotEmptyReturnType 523 | */ 524 | public function unlessNotEmpty(callable $callback, callable $default = null) 525 | { 526 | return $this->whenEmpty($callback, $default); 527 | } 528 | 529 | /** 530 | * Filter items by the given key value pair. 531 | * 532 | * @param callable|string $key 533 | * @param mixed $operator 534 | * @param mixed $value 535 | * @return static 536 | */ 537 | public function where($key, $operator = null, $value = null) 538 | { 539 | return $this->filter($this->operatorForWhere(...func_get_args())); 540 | } 541 | 542 | /** 543 | * Filter items where the value for the given key is null. 544 | * 545 | * @param string|null $key 546 | * @return static 547 | */ 548 | public function whereNull($key = null) 549 | { 550 | return $this->whereStrict($key, null); 551 | } 552 | 553 | /** 554 | * Filter items where the value for the given key is not null. 555 | * 556 | * @param string|null $key 557 | * @return static 558 | */ 559 | public function whereNotNull($key = null) 560 | { 561 | return $this->where($key, '!==', null); 562 | } 563 | 564 | /** 565 | * Filter items by the given key value pair using strict comparison. 566 | * 567 | * @param string $key 568 | * @param mixed $value 569 | * @return static 570 | */ 571 | public function whereStrict($key, $value) 572 | { 573 | return $this->where($key, '===', $value); 574 | } 575 | 576 | /** 577 | * Filter items by the given key value pair. 578 | * 579 | * @param string $key 580 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 581 | * @param bool $strict 582 | * @return static 583 | */ 584 | public function whereIn($key, $values, $strict = false) 585 | { 586 | $values = $this->getArrayableItems($values); 587 | 588 | return $this->filter(fn ($item) => in_array(data_get($item, $key), $values, $strict)); 589 | } 590 | 591 | /** 592 | * Filter items by the given key value pair using strict comparison. 593 | * 594 | * @param string $key 595 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 596 | * @return static 597 | */ 598 | public function whereInStrict($key, $values) 599 | { 600 | return $this->whereIn($key, $values, true); 601 | } 602 | 603 | /** 604 | * Filter items such that the value of the given key is between the given values. 605 | * 606 | * @param string $key 607 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 608 | * @return static 609 | */ 610 | public function whereBetween($key, $values) 611 | { 612 | return $this->where($key, '>=', reset($values))->where($key, '<=', end($values)); 613 | } 614 | 615 | /** 616 | * Filter items such that the value of the given key is not between the given values. 617 | * 618 | * @param string $key 619 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 620 | * @return static 621 | */ 622 | public function whereNotBetween($key, $values) 623 | { 624 | return $this->filter( 625 | fn ($item) => data_get($item, $key) < reset($values) || data_get($item, $key) > end($values) 626 | ); 627 | } 628 | 629 | /** 630 | * Filter items by the given key value pair. 631 | * 632 | * @param string $key 633 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 634 | * @param bool $strict 635 | * @return static 636 | */ 637 | public function whereNotIn($key, $values, $strict = false) 638 | { 639 | $values = $this->getArrayableItems($values); 640 | 641 | return $this->reject(fn ($item) => in_array(data_get($item, $key), $values, $strict)); 642 | } 643 | 644 | /** 645 | * Filter items by the given key value pair using strict comparison. 646 | * 647 | * @param string $key 648 | * @param \QL\Collect\Contracts\Support\Arrayable|iterable $values 649 | * @return static 650 | */ 651 | public function whereNotInStrict($key, $values) 652 | { 653 | return $this->whereNotIn($key, $values, true); 654 | } 655 | 656 | /** 657 | * Filter the items, removing any items that don't match the given type(s). 658 | * 659 | * @template TWhereInstanceOf 660 | * 661 | * @param class-string|array> $type 662 | * @return static 663 | */ 664 | public function whereInstanceOf($type) 665 | { 666 | return $this->filter(function ($value) use ($type) { 667 | if (is_array($type)) { 668 | foreach ($type as $classType) { 669 | if ($value instanceof $classType) { 670 | return true; 671 | } 672 | } 673 | 674 | return false; 675 | } 676 | 677 | return $value instanceof $type; 678 | }); 679 | } 680 | 681 | /** 682 | * Pass the collection to the given callback and return the result. 683 | * 684 | * @template TPipeReturnType 685 | * 686 | * @param callable($this): TPipeReturnType $callback 687 | * @return TPipeReturnType 688 | */ 689 | public function pipe(callable $callback) 690 | { 691 | return $callback($this); 692 | } 693 | 694 | /** 695 | * Pass the collection into a new class. 696 | * 697 | * @param class-string $class 698 | * @return mixed 699 | */ 700 | public function pipeInto($class) 701 | { 702 | return new $class($this); 703 | } 704 | 705 | /** 706 | * Pass the collection through a series of callable pipes and return the result. 707 | * 708 | * @param array $callbacks 709 | * @return mixed 710 | */ 711 | public function pipeThrough($callbacks) 712 | { 713 | return Collection::make($callbacks)->reduce( 714 | fn ($carry, $callback) => $callback($carry), 715 | $this, 716 | ); 717 | } 718 | 719 | /** 720 | * Reduce the collection to a single value. 721 | * 722 | * @template TReduceInitial 723 | * @template TReduceReturnType 724 | * 725 | * @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback 726 | * @param TReduceInitial $initial 727 | * @return TReduceReturnType 728 | */ 729 | public function reduce(callable $callback, $initial = null) 730 | { 731 | $result = $initial; 732 | 733 | foreach ($this as $key => $value) { 734 | $result = $callback($result, $value, $key); 735 | } 736 | 737 | return $result; 738 | } 739 | 740 | /** 741 | * Reduce the collection to multiple aggregate values. 742 | * 743 | * @param callable $callback 744 | * @param mixed ...$initial 745 | * @return array 746 | * 747 | * @throws \UnexpectedValueException 748 | */ 749 | public function reduceSpread(callable $callback, ...$initial) 750 | { 751 | $result = $initial; 752 | 753 | foreach ($this as $key => $value) { 754 | $result = call_user_func_array($callback, array_merge($result, [$value, $key])); 755 | 756 | if (! is_array($result)) { 757 | throw new UnexpectedValueException(sprintf( 758 | "%s::reduceSpread expects reducer to return an array, but got a '%s' instead.", 759 | class_basename(static::class), gettype($result) 760 | )); 761 | } 762 | } 763 | 764 | return $result; 765 | } 766 | 767 | /** 768 | * Create a collection of all elements that do not pass a given truth test. 769 | * 770 | * @param (callable(TValue, TKey): bool)|bool|TValue $callback 771 | * @return static 772 | */ 773 | public function reject($callback = true) 774 | { 775 | $useAsCallable = $this->useAsCallable($callback); 776 | 777 | return $this->filter(function ($value, $key) use ($callback, $useAsCallable) { 778 | return $useAsCallable 779 | ? ! $callback($value, $key) 780 | : $value != $callback; 781 | }); 782 | } 783 | 784 | /** 785 | * Pass the collection to the given callback and then return it. 786 | * 787 | * @param callable($this): mixed $callback 788 | * @return $this 789 | */ 790 | public function tap(callable $callback) 791 | { 792 | $callback($this); 793 | 794 | return $this; 795 | } 796 | 797 | /** 798 | * Return only unique items from the collection array. 799 | * 800 | * @param (callable(TValue, TKey): mixed)|string|null $key 801 | * @param bool $strict 802 | * @return static 803 | */ 804 | public function unique($key = null, $strict = false) 805 | { 806 | $callback = $this->valueRetriever($key); 807 | 808 | $exists = []; 809 | 810 | return $this->reject(function ($item, $key) use ($callback, $strict, &$exists) { 811 | if (in_array($id = $callback($item, $key), $exists, $strict)) { 812 | return true; 813 | } 814 | 815 | $exists[] = $id; 816 | }); 817 | } 818 | 819 | /** 820 | * Return only unique items from the collection array using strict comparison. 821 | * 822 | * @param (callable(TValue, TKey): mixed)|string|null $key 823 | * @return static 824 | */ 825 | public function uniqueStrict($key = null) 826 | { 827 | return $this->unique($key, true); 828 | } 829 | 830 | /** 831 | * Collect the values into a collection. 832 | * 833 | * @return \QL\Collect\Support\Collection 834 | */ 835 | public function collect() 836 | { 837 | return new Collection($this->all()); 838 | } 839 | 840 | /** 841 | * Get the collection of items as a plain array. 842 | * 843 | * @return array 844 | */ 845 | public function toArray() 846 | { 847 | return $this->map(fn ($value) => $value instanceof Arrayable ? $value->toArray() : $value)->all(); 848 | } 849 | 850 | /** 851 | * Convert the object into something JSON serializable. 852 | * 853 | * @return array 854 | */ 855 | public function jsonSerialize(): array 856 | { 857 | return array_map(function ($value) { 858 | if ($value instanceof JsonSerializable) { 859 | return $value->jsonSerialize(); 860 | } elseif ($value instanceof Jsonable) { 861 | return json_decode($value->toJson(), true); 862 | } elseif ($value instanceof Arrayable) { 863 | return $value->toArray(); 864 | } 865 | 866 | return $value; 867 | }, $this->all()); 868 | } 869 | 870 | /** 871 | * Get the collection of items as JSON. 872 | * 873 | * @param int $options 874 | * @return string 875 | */ 876 | public function toJson($options = 0) 877 | { 878 | return json_encode($this->jsonSerialize(), $options); 879 | } 880 | 881 | /** 882 | * Get a CachingIterator instance. 883 | * 884 | * @param int $flags 885 | * @return \CachingIterator 886 | */ 887 | public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING) 888 | { 889 | return new CachingIterator($this->getIterator(), $flags); 890 | } 891 | 892 | /** 893 | * Convert the collection to its string representation. 894 | * 895 | * @return string 896 | */ 897 | public function __toString() 898 | { 899 | return $this->escapeWhenCastingToString 900 | ? e($this->toJson()) 901 | : $this->toJson(); 902 | } 903 | 904 | /** 905 | * Indicate that the model's string representation should be escaped when __toString is invoked. 906 | * 907 | * @param bool $escape 908 | * @return $this 909 | */ 910 | public function escapeWhenCastingToString($escape = true) 911 | { 912 | $this->escapeWhenCastingToString = $escape; 913 | 914 | return $this; 915 | } 916 | 917 | /** 918 | * Add a method to the list of proxied methods. 919 | * 920 | * @param string $method 921 | * @return void 922 | */ 923 | public static function proxy($method) 924 | { 925 | static::$proxies[] = $method; 926 | } 927 | 928 | /** 929 | * Dynamically access collection proxies. 930 | * 931 | * @param string $key 932 | * @return mixed 933 | * 934 | * @throws \Exception 935 | */ 936 | public function __get($key) 937 | { 938 | if (! in_array($key, static::$proxies)) { 939 | throw new Exception("Property [{$key}] does not exist on this collection instance."); 940 | } 941 | 942 | return new HigherOrderCollectionProxy($this, $key); 943 | } 944 | 945 | /** 946 | * Results array of items from Collection or Arrayable. 947 | * 948 | * @param mixed $items 949 | * @return array 950 | */ 951 | protected function getArrayableItems($items) 952 | { 953 | if (is_array($items)) { 954 | return $items; 955 | } elseif ($items instanceof Enumerable) { 956 | return $items->all(); 957 | } elseif ($items instanceof Arrayable) { 958 | return $items->toArray(); 959 | } elseif ($items instanceof Traversable) { 960 | return iterator_to_array($items); 961 | } elseif ($items instanceof Jsonable) { 962 | return json_decode($items->toJson(), true); 963 | } elseif ($items instanceof JsonSerializable) { 964 | return (array) $items->jsonSerialize(); 965 | } elseif ($items instanceof UnitEnum) { 966 | return [$items]; 967 | } 968 | 969 | return (array) $items; 970 | } 971 | 972 | /** 973 | * Get an operator checker callback. 974 | * 975 | * @param callable|string $key 976 | * @param string|null $operator 977 | * @param mixed $value 978 | * @return \Closure 979 | */ 980 | protected function operatorForWhere($key, $operator = null, $value = null) 981 | { 982 | if ($this->useAsCallable($key)) { 983 | return $key; 984 | } 985 | 986 | if (func_num_args() === 1) { 987 | $value = true; 988 | 989 | $operator = '='; 990 | } 991 | 992 | if (func_num_args() === 2) { 993 | $value = $operator; 994 | 995 | $operator = '='; 996 | } 997 | 998 | return function ($item) use ($key, $operator, $value) { 999 | $retrieved = data_get($item, $key); 1000 | 1001 | $strings = array_filter([$retrieved, $value], function ($value) { 1002 | return is_string($value) || (is_object($value) && method_exists($value, '__toString')); 1003 | }); 1004 | 1005 | if (count($strings) < 2 && count(array_filter([$retrieved, $value], 'is_object')) == 1) { 1006 | return in_array($operator, ['!=', '<>', '!==']); 1007 | } 1008 | 1009 | switch ($operator) { 1010 | default: 1011 | case '=': 1012 | case '==': return $retrieved == $value; 1013 | case '!=': 1014 | case '<>': return $retrieved != $value; 1015 | case '<': return $retrieved < $value; 1016 | case '>': return $retrieved > $value; 1017 | case '<=': return $retrieved <= $value; 1018 | case '>=': return $retrieved >= $value; 1019 | case '===': return $retrieved === $value; 1020 | case '!==': return $retrieved !== $value; 1021 | case '<=>': return $retrieved <=> $value; 1022 | } 1023 | }; 1024 | } 1025 | 1026 | /** 1027 | * Determine if the given value is callable, but not a string. 1028 | * 1029 | * @param mixed $value 1030 | * @return bool 1031 | */ 1032 | protected function useAsCallable($value) 1033 | { 1034 | return ! is_string($value) && is_callable($value); 1035 | } 1036 | 1037 | /** 1038 | * Get a value retrieving callback. 1039 | * 1040 | * @param callable|string|null $value 1041 | * @return callable 1042 | */ 1043 | protected function valueRetriever($value) 1044 | { 1045 | if ($this->useAsCallable($value)) { 1046 | return $value; 1047 | } 1048 | 1049 | return fn ($item) => data_get($item, $value); 1050 | } 1051 | 1052 | /** 1053 | * Make a function to check an item's equality. 1054 | * 1055 | * @param mixed $value 1056 | * @return \Closure(mixed): bool 1057 | */ 1058 | protected function equality($value) 1059 | { 1060 | return fn ($item) => $item === $value; 1061 | } 1062 | 1063 | /** 1064 | * Make a function using another function, by negating its result. 1065 | * 1066 | * @param \Closure $callback 1067 | * @return \Closure 1068 | */ 1069 | protected function negate(Closure $callback) 1070 | { 1071 | return fn (...$params) => ! $callback(...$params); 1072 | } 1073 | 1074 | /** 1075 | * Make a function that returns what's passed to it. 1076 | * 1077 | * @return \Closure(TValue): TValue 1078 | */ 1079 | protected function identity() 1080 | { 1081 | return fn ($value) => $value; 1082 | } 1083 | } 1084 | -------------------------------------------------------------------------------- /src/Collect/Support/Traits/Macroable.php: -------------------------------------------------------------------------------- 1 | getMethods( 43 | ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED 44 | ); 45 | 46 | foreach ($methods as $method) { 47 | if ($replace || ! static::hasMacro($method->name)) { 48 | $method->setAccessible(true); 49 | static::macro($method->name, $method->invoke($mixin)); 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * Checks if macro is registered. 56 | * 57 | * @param string $name 58 | * @return bool 59 | */ 60 | public static function hasMacro($name) 61 | { 62 | return isset(static::$macros[$name]); 63 | } 64 | 65 | /** 66 | * Flush the existing macros. 67 | * 68 | * @return void 69 | */ 70 | public static function flushMacros() 71 | { 72 | static::$macros = []; 73 | } 74 | 75 | /** 76 | * Dynamically handle calls to the class. 77 | * 78 | * @param string $method 79 | * @param array $parameters 80 | * @return mixed 81 | * 82 | * @throws \BadMethodCallException 83 | */ 84 | public static function __callStatic($method, $parameters) 85 | { 86 | if (! static::hasMacro($method)) { 87 | throw new BadMethodCallException(sprintf( 88 | 'Method %s::%s does not exist.', static::class, $method 89 | )); 90 | } 91 | 92 | $macro = static::$macros[$method]; 93 | 94 | if ($macro instanceof Closure) { 95 | $macro = $macro->bindTo(null, static::class); 96 | } 97 | 98 | return $macro(...$parameters); 99 | } 100 | 101 | /** 102 | * Dynamically handle calls to the class. 103 | * 104 | * @param string $method 105 | * @param array $parameters 106 | * @return mixed 107 | * 108 | * @throws \BadMethodCallException 109 | */ 110 | public function __call($method, $parameters) 111 | { 112 | if (! static::hasMacro($method)) { 113 | throw new BadMethodCallException(sprintf( 114 | 'Method %s::%s does not exist.', static::class, $method 115 | )); 116 | } 117 | 118 | $macro = static::$macros[$method]; 119 | 120 | if ($macro instanceof Closure) { 121 | $macro = $macro->bindTo($this, static::class); 122 | } 123 | 124 | return $macro(...$parameters); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Collect/Support/Traits/Tappable.php: -------------------------------------------------------------------------------- 1 | Illuminate\Contracts\Support\Arrayable::class, 5 | QL\Collect\Contracts\Support\Jsonable::class => Illuminate\Contracts\Support\Jsonable::class, 6 | QL\Collect\Contracts\Support\Htmlable::class => Illuminate\Contracts\Support\Htmlable::class, 7 | QL\Collect\Contracts\Support\CanBeEscapedWhenCastToString::class => Illuminate\Contracts\Support\CanBeEscapedWhenCastToString::class, 8 | QL\Collect\Support\Arr::class => Illuminate\Support\Arr::class, 9 | QL\Collect\Support\Collection::class => Illuminate\Support\Collection::class, 10 | QL\Collect\Support\Enumerable::class => Illuminate\Support\Enumerable::class, 11 | QL\Collect\Support\HigherOrderCollectionProxy::class => Illuminate\Support\HigherOrderCollectionProxy::class, 12 | QL\Collect\Support\LazyCollection::class => Illuminate\Support\LazyCollection::class, 13 | QL\Collect\Support\Traits\EnumeratesValues::class => Illuminate\Support\Traits\EnumeratesValues::class, 14 | ]; 15 | 16 | # echo "\n\n-- Aliasing....\n---------------------------------------------\n\n"; 17 | 18 | foreach ($aliases as $tighten => $illuminate) { 19 | if (! class_exists($illuminate) && ! interface_exists($illuminate) && ! trait_exists($illuminate)) { 20 | # echo "Aliasing {$tighten} to {$illuminate}.\n"; 21 | class_alias($tighten, $illuminate); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Collect/Support/helpers.php: -------------------------------------------------------------------------------- 1 | $segment) { 53 | unset($key[$i]); 54 | 55 | if (is_null($segment)) { 56 | return $target; 57 | } 58 | 59 | if ($segment === '*') { 60 | if ($target instanceof Collection) { 61 | $target = $target->all(); 62 | } elseif (! is_array($target)) { 63 | return value($default); 64 | } 65 | 66 | $result = []; 67 | 68 | foreach ($target as $item) { 69 | $result[] = data_get($item, $key); 70 | } 71 | 72 | return in_array('*', $key) ? Arr::collapse($result) : $result; 73 | } 74 | 75 | if (Arr::accessible($target) && Arr::exists($target, $segment)) { 76 | $target = $target[$segment]; 77 | } elseif (is_object($target) && isset($target->{$segment})) { 78 | $target = $target->{$segment}; 79 | } else { 80 | return value($default); 81 | } 82 | } 83 | 84 | return $target; 85 | } 86 | } 87 | 88 | if (! function_exists('tap')) { 89 | /** 90 | * Call the given Closure with the given value then return the value. 91 | * 92 | * @param mixed $value 93 | * @param callable|null $callback 94 | * @return mixed 95 | */ 96 | function tap($value, $callback = null) 97 | { 98 | if (is_null($callback)) { 99 | return new HigherOrderTapProxy($value); 100 | } 101 | 102 | $callback($value); 103 | 104 | return $value; 105 | } 106 | } 107 | 108 | if (! function_exists('class_basename')) { 109 | /** 110 | * Get the class "basename" of the given object / class. 111 | * 112 | * @param string|object $class 113 | * @return string 114 | */ 115 | function class_basename($class) 116 | { 117 | $class = is_object($class) ? get_class($class) : $class; 118 | 119 | return basename(str_replace('\\', '/', $class)); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/22 6 | */ 7 | 8 | namespace QL; 9 | use Closure; 10 | use QL\Collect\Support\Collection; 11 | 12 | class Config 13 | { 14 | protected static $instance = null; 15 | 16 | protected $plugins; 17 | protected $binds; 18 | 19 | /** 20 | * Config constructor. 21 | */ 22 | public function __construct() 23 | { 24 | $this->plugins = new Collection(); 25 | $this->binds = new Collection(); 26 | } 27 | 28 | 29 | /** 30 | * Get the Config instance 31 | * 32 | * @return null|Config 33 | */ 34 | public static function getInstance() 35 | { 36 | self::$instance || self::$instance = new self(); 37 | return self::$instance; 38 | } 39 | 40 | /** 41 | * Global installation plugin 42 | * 43 | * @param $plugins 44 | * @param array ...$opt 45 | * @return $this 46 | */ 47 | public function use($plugins,...$opt) 48 | { 49 | if(is_string($plugins)){ 50 | $this->plugins->push([$plugins,$opt]); 51 | }else{ 52 | $this->plugins = $this->plugins->merge($plugins); 53 | } 54 | return $this; 55 | } 56 | 57 | /** 58 | * Global binding custom method 59 | * 60 | * @param string $name 61 | * @param Closure $provider 62 | * @return $this 63 | */ 64 | public function bind(string $name, Closure $provider) 65 | { 66 | $this->binds[$name] = $provider; 67 | return $this; 68 | } 69 | 70 | public function bootstrap(QueryList $queryList) 71 | { 72 | $this->installPlugins($queryList); 73 | $this->installBind($queryList); 74 | } 75 | 76 | protected function installPlugins(QueryList $queryList) 77 | { 78 | $this->plugins->each(function($plugin) use($queryList){ 79 | if(is_string($plugin)){ 80 | $queryList->use($plugin); 81 | }else{ 82 | $queryList->use($plugin[0],...$plugin[1]); 83 | } 84 | }); 85 | } 86 | 87 | protected function installBind(QueryList $queryList) 88 | { 89 | $this->binds->each(function ($provider,$name) use($queryList){ 90 | $queryList->bind($name,$provider); 91 | }); 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /src/Contracts/PluginContract.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/22 6 | */ 7 | 8 | namespace QL\Contracts; 9 | 10 | use QL\QueryList; 11 | 12 | interface PluginContract 13 | { 14 | public static function install(QueryList $queryList,...$opt); 15 | } -------------------------------------------------------------------------------- /src/Contracts/ServiceProviderContract.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/20 6 | */ 7 | 8 | namespace QL\Contracts; 9 | 10 | use QL\Kernel; 11 | 12 | interface ServiceProviderContract 13 | { 14 | public function register(Kernel $kernel); 15 | } -------------------------------------------------------------------------------- /src/Dom/Dom.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/19 6 | */ 7 | 8 | namespace QL\Dom; 9 | 10 | use phpQueryObject; 11 | 12 | class Dom 13 | { 14 | 15 | protected $document; 16 | 17 | /** 18 | * Dom constructor. 19 | */ 20 | public function __construct(phpQueryObject $document) 21 | { 22 | $this->document = $document; 23 | } 24 | 25 | public function find($selector) 26 | { 27 | $elements = $this->document->find($selector); 28 | return new Elements($elements); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Dom/Elements.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/19 6 | */ 7 | 8 | namespace QL\Dom; 9 | 10 | use phpDocumentor\Reflection\Types\Null_; 11 | use phpQueryObject; 12 | use QL\Collect\Support\Collection; 13 | 14 | /** 15 | * Class Elements 16 | * @package QL\Dom 17 | * 18 | * @method Elements toReference(&$var) 19 | * @method Elements documentFragment($state = null) 20 | * @method Elements toRoot() 21 | * @method Elements getDocumentIDRef(&$documentID) 22 | * @method Elements getDocument() 23 | * @method \DOMDocument getDOMDocument() 24 | * @method Elements getDocumentID() 25 | * @method Elements unloadDocument() 26 | * @method bool isHTML() 27 | * @method bool isXHTML() 28 | * @method bool isXML() 29 | * @method string serialize() 30 | * @method array serializeArray($submit = null) 31 | * @method \DOMElement|\DOMElement[] get($index = null, $callback1 = null, $callback2 = null, $callback3 = null) 32 | * @method string|array getString($index = null, $callback1 = null, $callback2 = null, $callback3 = null) 33 | * @method string|array getStrings($index = null, $callback1 = null, $callback2 = null, $callback3 = null) 34 | * @method Elements newInstance($newStack = null) 35 | * @method Elements find($selectors, $context = null, $noHistory = false) 36 | * @method Elements|bool is($selector, $nodes = null) 37 | * @method Elements filterCallback($callback, $_skipHistory = false) 38 | * @method Elements filter($selectors, $_skipHistory = false) 39 | * @method Elements load($url, $data = null, $callback = null) 40 | * @method Elements trigger($type, $data = []) 41 | * @method Elements triggerHandler($type, $data = []) 42 | * @method Elements bind($type, $data, $callback = null) 43 | * @method Elements unbind($type = null, $callback = null) 44 | * @method Elements change($callback = null) 45 | * @method Elements submit($callback = null) 46 | * @method Elements click($callback = null) 47 | * @method Elements wrapAllOld($wrapper) 48 | * @method Elements wrapAll($wrapper) 49 | * @method Elements wrapAllPHP($codeBefore, $codeAfter) 50 | * @method Elements wrap($wrapper) 51 | * @method Elements wrapPHP($codeBefore, $codeAfter) 52 | * @method Elements wrapInner($wrapper) 53 | * @method Elements wrapInnerPHP($codeBefore, $codeAfter) 54 | * @method Elements contents() 55 | * @method Elements contentsUnwrap() 56 | * @method Elements switchWith($markup) 57 | * @method Elements eq($num) 58 | * @method Elements size() 59 | * @method Elements length() 60 | * @method int count() 61 | * @method Elements end($level = 1) 62 | * @method Elements _clone() 63 | * @method Elements replaceWithPHP($code) 64 | * @method Elements replaceWith($content) 65 | * @method Elements replaceAll($selector) 66 | * @method Elements remove($selector = null) 67 | * @method Elements|string markup($markup = null, $callback1 = null, $callback2 = null, $callback3 = null) 68 | * @method string markupOuter($callback1 = null, $callback2 = null, $callback3 = null) 69 | * @method Elements|string html($html = null, $callback1 = null, $callback2 = null, $callback3 = null) 70 | * @method Elements|string xml($xml = null, $callback1 = null, $callback2 = null, $callback3 = null) 71 | * @method string htmlOuter($callback1 = null, $callback2 = null, $callback3 = null) 72 | * @method string xmlOuter($callback1 = null, $callback2 = null, $callback3 = null) 73 | * @method Elements php($code) 74 | * @method string markupPHP($code) 75 | * @method string markupOuterPHP() 76 | * @method Elements children($selector) 77 | * @method Elements ancestors($selector) 78 | * @method Elements append($content) 79 | * @method Elements appendPHP($content) 80 | * @method Elements appendTo($seletor) 81 | * @method Elements prepend($content) 82 | * @method Elements prependPHP($content) 83 | * @method Elements prependTo($seletor) 84 | * @method Elements before($content) 85 | * @method Elements beforePHP($content) 86 | * @method Elements insertBefore($seletor) 87 | * @method Elements after($content) 88 | * @method Elements afterPHP($content) 89 | * @method Elements insertAfter($seletor) 90 | * @method Elements insert($target, $type) 91 | * @method int index($subject) 92 | * @method Elements slice($start, $end = null) 93 | * @method Elements reverse() 94 | * @method Elements|string text($text = null, $callback1 = null, $callback2 = null, $callback3 = null) 95 | * @method Elements plugin($class, $file = null) 96 | * @method Elements _next($selector = null) 97 | * @method Elements _prev($selector = null) 98 | * @method Elements prev($selector = null) 99 | * @method Elements prevAll($selector = null) 100 | * @method Elements nextAll($selector = null) 101 | * @method Elements siblings($selector = null) 102 | * @method Elements not($selector = null) 103 | * @method Elements add($selector = null) 104 | * @method Elements parent($selector = null) 105 | * @method Elements parents($selector = null) 106 | * @method Elements stack($nodeTypes = null) 107 | * @method Elements|string attr($attr = null, $value = null) 108 | * @method Elements attrPHP($attr, $code) 109 | * @method Elements removeAttr($attr) 110 | * @method Elements|string val($val = null) 111 | * @method Elements andSelf() 112 | * @method Elements addClass($className) 113 | * @method Elements addClassPHP($className) 114 | * @method bool hasClass($className) 115 | * @method Elements removeClass($className) 116 | * @method Elements toggleClass($className) 117 | * @method Elements _empty() 118 | * @method Elements callback($callback, $param1 = null, $param2 = null, $param3 = null) 119 | * @method string data($key, $value = null) 120 | * @method Elements removeData($key) 121 | * @method void rewind() 122 | * @method Elements current() 123 | * @method int key() 124 | * @method Elements next($cssSelector = null) 125 | * @method bool valid() 126 | * @method bool offsetExists($offset) 127 | * @method Elements offsetGet($offset) 128 | * @method void offsetSet($offset, $value) 129 | * @method string whois($oneNode) 130 | * @method Elements dump() 131 | * @method Elements dumpWhois() 132 | * @method Elements dumpLength() 133 | * @method Elements dumpTree($html, $title) 134 | * @method dumpDie() 135 | */ 136 | class Elements 137 | { 138 | /** 139 | * @var phpQueryObject 140 | */ 141 | protected $elements; 142 | 143 | /** 144 | * Elements constructor. 145 | * @param $elements 146 | */ 147 | public function __construct(phpQueryObject $elements) 148 | { 149 | $this->elements = $elements; 150 | } 151 | 152 | public function __get($name) 153 | { 154 | return property_exists($this->elements, $name) ? $this->elements->$name : $this->elements->attr($name); 155 | } 156 | 157 | public function __call($name, $arguments) 158 | { 159 | $obj = call_user_func_array([$this->elements, $name], $arguments); 160 | if ($obj instanceof phpQueryObject) { 161 | $obj = new self($obj); 162 | } else if (is_string($obj)) { 163 | $obj = trim($obj); 164 | } 165 | return $obj; 166 | } 167 | 168 | /** 169 | * Iterating elements 170 | * 171 | * @param callable $callback 172 | * 173 | * @return $this 174 | */ 175 | public function each(callable $callback) 176 | { 177 | foreach ($this->elements as $key => $element) { 178 | $break = $callback(new self(pq($element)), $key); 179 | if ($break === false) { 180 | break; 181 | } 182 | } 183 | 184 | return $this; 185 | } 186 | 187 | /** 188 | * Iterating elements 189 | * 190 | * @param $callback 191 | * @return \Illuminate\Support\Collection|\QL\Collect\Support\Collection 192 | */ 193 | public function map($callback) 194 | { 195 | $collection = new Collection(); 196 | $this->elements->each(function ($dom) use (& $collection, $callback) { 197 | $collection->push($callback(new self(pq($dom)))); 198 | }); 199 | return $collection; 200 | } 201 | 202 | /** 203 | * Gets the attributes of all the elements 204 | * 205 | * @param string $attr HTML attribute name 206 | * @return \Illuminate\Support\Collection|\QL\Collect\Support\Collection 207 | */ 208 | public function attrs($attr) 209 | { 210 | return $this->map(function ($item) use ($attr) { 211 | return $item->attr($attr); 212 | }); 213 | } 214 | 215 | /** 216 | * Gets the text of all the elements 217 | * 218 | * @return \Illuminate\Support\Collection|\QL\Collect\Support\Collection 219 | */ 220 | public function texts() 221 | { 222 | return $this->map(function ($item) { 223 | return trim($item->text()); 224 | }); 225 | } 226 | 227 | /** 228 | * Gets the html of all the elements 229 | * 230 | * @return \Illuminate\Support\Collection|\QL\Collect\Support\Collection 231 | */ 232 | public function htmls() 233 | { 234 | return $this->map(function ($item) { 235 | return trim($item->html()); 236 | }); 237 | } 238 | 239 | /** 240 | * Gets the htmlOuter of all the elements 241 | * 242 | * @return \Illuminate\Support\Collection|\QL\Collect\Support\Collection 243 | */ 244 | public function htmlOuters() 245 | { 246 | return $this->map(function ($item) { 247 | return trim($item->htmlOuter()); 248 | }); 249 | } 250 | 251 | 252 | /** 253 | * @return phpQueryObject 254 | */ 255 | public function getElements(): phpQueryObject 256 | { 257 | return $this->elements; 258 | } 259 | 260 | } -------------------------------------------------------------------------------- /src/Dom/Query.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/21 6 | */ 7 | 8 | namespace QL\Dom; 9 | 10 | use QL\Collect\Support\Collection; 11 | use phpQuery; 12 | use phpQueryObject; 13 | use QL\QueryList; 14 | use Closure; 15 | 16 | class Query 17 | { 18 | protected $html; 19 | /** 20 | * @var \phpQueryObject 21 | */ 22 | protected $document; 23 | protected $rules; 24 | protected $range = null; 25 | protected $ql; 26 | /** 27 | * @var Collection 28 | */ 29 | protected $data; 30 | 31 | 32 | public function __construct(QueryList $ql) 33 | { 34 | $this->ql = $ql; 35 | } 36 | 37 | /** 38 | * @param bool $rel 39 | * @return String 40 | */ 41 | public function getHtml($rel = true) 42 | { 43 | return $rel ? $this->document->htmlOuter() : $this->html; 44 | } 45 | 46 | /** 47 | * @param $html 48 | * @param null $charset 49 | * @return QueryList 50 | */ 51 | public function setHtml($html, $charset = null) 52 | { 53 | $this->html = value($html); 54 | $this->destroyDocument(); 55 | $this->document = phpQuery::newDocumentHTML($this->html, $charset); 56 | return $this->ql; 57 | } 58 | 59 | /** 60 | * Get crawl results 61 | * 62 | * @param Closure|null $callback 63 | * @return Collection|static 64 | */ 65 | public function getData(?Closure $callback = null) 66 | { 67 | return $this->handleData($this->data, $callback); 68 | } 69 | 70 | /** 71 | * @param Collection $data 72 | */ 73 | public function setData(Collection $data) 74 | { 75 | $this->data = $data; 76 | } 77 | 78 | 79 | /** 80 | * Searches for all elements that match the specified expression. 81 | * 82 | * @param $selector A string containing a selector expression to match elements against. 83 | * @return Elements 84 | */ 85 | public function find($selector) 86 | { 87 | return (new Dom($this->document))->find($selector); 88 | } 89 | 90 | /** 91 | * Set crawl rule 92 | * 93 | * $rules = [ 94 | * 'rule_name1' => ['selector','HTML attribute | text | html','Tag filter list','callback'], 95 | * 'rule_name2' => ['selector','HTML attribute | text | html','Tag filter list','callback'], 96 | * // ... 97 | * ] 98 | * 99 | * @param array $rules 100 | * @return QueryList 101 | */ 102 | public function rules(array $rules) 103 | { 104 | $this->rules = $rules; 105 | return $this->ql; 106 | } 107 | 108 | 109 | /** 110 | * Set the slice area for crawl list 111 | * 112 | * @param $selector 113 | * @return QueryList 114 | */ 115 | public function range($selector) 116 | { 117 | $this->range = $selector; 118 | return $this->ql; 119 | } 120 | 121 | /** 122 | * Remove HTML head,try to solve the garbled 123 | * 124 | * @return QueryList 125 | */ 126 | public function removeHead() 127 | { 128 | $html = preg_replace('/(|).+?<\/head>/is', '', $this->html); 129 | $html && $this->setHtml($html); 130 | return $this->ql; 131 | } 132 | 133 | /** 134 | * Execute the query rule 135 | * 136 | * @param Closure|null $callback 137 | * @return QueryList 138 | */ 139 | public function query(?Closure $callback = null) 140 | { 141 | $this->data = $this->getList(); 142 | $this->data = $this->handleData($this->data, $callback); 143 | return $this->ql; 144 | } 145 | 146 | public function handleData(Collection $data, $callback) 147 | { 148 | if (is_callable($callback)) { 149 | if (empty($this->range)) { 150 | $data = new Collection($callback($data->all(), null)); 151 | } else { 152 | $data = $data->map($callback); 153 | } 154 | } 155 | 156 | return $data; 157 | } 158 | 159 | protected function getList() 160 | { 161 | $data = []; 162 | if (empty($this->range)) { 163 | foreach ($this->rules as $key => $reg_value) { 164 | $rule = $this->parseRule($reg_value); 165 | $contentElements = $this->document->find($rule['selector']); 166 | $data[$key] = $this->extractContent($contentElements, $key, $rule); 167 | } 168 | } else { 169 | $rangeElements = $this->document->find($this->range); 170 | $i = 0; 171 | foreach ($rangeElements as $element) { 172 | foreach ($this->rules as $key => $reg_value) { 173 | $rule = $this->parseRule($reg_value); 174 | $contentElements = pq($element)->find($rule['selector']); 175 | $data[$i][$key] = $this->extractContent($contentElements, $key, $rule); 176 | } 177 | $i++; 178 | } 179 | } 180 | 181 | return new Collection($data); 182 | } 183 | 184 | protected function extractContent(phpQueryObject $pqObj, $ruleName, $rule) 185 | { 186 | switch ($rule['attr']) { 187 | case 'text': 188 | $content = $this->allowTags($pqObj->html(), $rule['filter_tags']); 189 | break; 190 | case 'texts': 191 | $content = (new Elements($pqObj))->map(function (Elements $element) use ($rule) { 192 | return $this->allowTags($element->html(), $rule['filter_tags']); 193 | })->all(); 194 | break; 195 | case 'html': 196 | $content = $this->stripTags($pqObj->html(), $rule['filter_tags']); 197 | break; 198 | case 'htmls': 199 | $content = (new Elements($pqObj))->map(function (Elements $element) use ($rule) { 200 | return $this->stripTags($element->html(), $rule['filter_tags']); 201 | })->all(); 202 | break; 203 | case 'htmlOuter': 204 | $content = $this->stripTags($pqObj->htmlOuter(), $rule['filter_tags']); 205 | break; 206 | case 'htmlOuters': 207 | $content = (new Elements($pqObj))->map(function (Elements $element) use ($rule) { 208 | return $this->stripTags($element->htmlOuter(), $rule['filter_tags']); 209 | })->all(); 210 | break; 211 | default: 212 | if(preg_match('/attr\((.+)\)/', $rule['attr'], $arr)) { 213 | $content = $pqObj->attr($arr[1]); 214 | } elseif (preg_match('/attrs\((.+)\)/', $rule['attr'], $arr)) { 215 | $content = (new Elements($pqObj))->attrs($arr[1])->all(); 216 | } else { 217 | $content = $pqObj->attr($rule['attr']); 218 | } 219 | break; 220 | } 221 | 222 | if (is_callable($rule['handle_callback'])) { 223 | $content = call_user_func($rule['handle_callback'], $content, $ruleName); 224 | } 225 | 226 | return $content; 227 | } 228 | 229 | protected function parseRule($rule) 230 | { 231 | $result = []; 232 | $result['selector'] = $rule[0]; 233 | $result['attr'] = $rule[1]; 234 | $result['filter_tags'] = $rule[2] ?? ''; 235 | $result['handle_callback'] = $rule[3] ?? null; 236 | 237 | return $result; 238 | } 239 | 240 | /** 241 | * 去除特定的html标签 242 | * @param string $html 243 | * @param string $tags_str 多个标签名之间用空格隔开 244 | * @return string 245 | */ 246 | protected function stripTags($html, $tags_str) 247 | { 248 | $tagsArr = $this->tag($tags_str); 249 | $html = $this->removeTags($html, $tagsArr[1]); 250 | $p = array(); 251 | foreach ($tagsArr[0] as $tag) { 252 | $p[] = "/(<(?:\/" . $tag . "|" . $tag . ")[^>]*>)/i"; 253 | } 254 | $html = preg_replace($p, "", trim($html)); 255 | return $html; 256 | } 257 | 258 | /** 259 | * 保留特定的html标签 260 | * @param string $html 261 | * @param string $tags_str 多个标签名之间用空格隔开 262 | * @return string 263 | */ 264 | protected function allowTags($html, $tags_str) 265 | { 266 | $tagsArr = $this->tag($tags_str); 267 | $html = $this->removeTags($html, $tagsArr[1]); 268 | $allow = ''; 269 | foreach ($tagsArr[0] as $tag) { 270 | $allow .= "<$tag> "; 271 | } 272 | return strip_tags(trim($html), $allow); 273 | } 274 | 275 | protected function tag($tags_str) 276 | { 277 | $tagArr = preg_split("/\s+/", $tags_str, -1, PREG_SPLIT_NO_EMPTY); 278 | $tags = array(array(), array()); 279 | foreach ($tagArr as $tag) { 280 | if (preg_match('/-(.+)/', $tag, $arr)) { 281 | array_push($tags[1], $arr[1]); 282 | } else { 283 | array_push($tags[0], $tag); 284 | } 285 | } 286 | return $tags; 287 | } 288 | 289 | /** 290 | * 移除特定的html标签 291 | * @param string $html 292 | * @param array $tags 标签数组 293 | * @return string 294 | */ 295 | protected function removeTags($html, $tags) 296 | { 297 | $tag_str = ''; 298 | if (count($tags)) { 299 | foreach ($tags as $tag) { 300 | $tag_str .= $tag_str ? ',' . $tag : $tag; 301 | } 302 | // phpQuery::$defaultCharset = $this->inputEncoding?$this->inputEncoding:$this->htmlEncoding; 303 | $doc = phpQuery::newDocumentHTML($html); 304 | pq($doc)->find($tag_str)->remove(); 305 | $html = pq($doc)->htmlOuter(); 306 | $doc->unloadDocument(); 307 | } 308 | return $html; 309 | } 310 | 311 | protected function destroyDocument() 312 | { 313 | if ($this->document instanceof phpQueryObject) { 314 | $this->document->unloadDocument(); 315 | } 316 | } 317 | 318 | public function __destruct() 319 | { 320 | $this->destroyDocument(); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/Exceptions/ServiceNotFoundException.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/21 6 | */ 7 | 8 | namespace QL\Exceptions; 9 | 10 | use Exception; 11 | 12 | class ServiceNotFoundException extends Exception 13 | { 14 | 15 | } -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/21 6 | */ 7 | 8 | namespace QL; 9 | 10 | use QL\Contracts\ServiceProviderContract; 11 | use QL\Exceptions\ServiceNotFoundException; 12 | use QL\Providers\EncodeServiceProvider; 13 | use Closure; 14 | use QL\Providers\HttpServiceProvider; 15 | use QL\Providers\PluginServiceProvider; 16 | use QL\Providers\SystemServiceProvider; 17 | use QL\Collect\Support\Collection; 18 | 19 | class Kernel 20 | { 21 | protected $providers = [ 22 | SystemServiceProvider::class, 23 | HttpServiceProvider::class, 24 | EncodeServiceProvider::class, 25 | PluginServiceProvider::class 26 | ]; 27 | 28 | protected $binds; 29 | protected $ql; 30 | 31 | /** 32 | * Kernel constructor. 33 | * @param $ql 34 | */ 35 | public function __construct(QueryList $ql) 36 | { 37 | $this->ql = $ql; 38 | $this->binds = new Collection(); 39 | } 40 | 41 | public function bootstrap() 42 | { 43 | //注册服务提供者 44 | $this->registerProviders(); 45 | return $this; 46 | } 47 | 48 | public function registerProviders() 49 | { 50 | foreach ($this->providers as $provider) { 51 | $this->register(new $provider()); 52 | } 53 | } 54 | 55 | public function bind(string $name,Closure $provider) 56 | { 57 | $this->binds[$name] = $provider; 58 | } 59 | 60 | public function getService(string $name) 61 | { 62 | if(!$this->binds->offsetExists($name)){ 63 | throw new ServiceNotFoundException("Service: {$name} not found!"); 64 | } 65 | return $this->binds[$name]; 66 | } 67 | 68 | private function register(ServiceProviderContract $instance) 69 | { 70 | $instance->register($this); 71 | } 72 | 73 | 74 | } -------------------------------------------------------------------------------- /src/Providers/EncodeServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/20 6 | */ 7 | 8 | namespace QL\Providers; 9 | 10 | use QL\Contracts\ServiceProviderContract; 11 | use QL\Kernel; 12 | use QL\Services\EncodeService; 13 | 14 | class EncodeServiceProvider implements ServiceProviderContract 15 | { 16 | public function register(Kernel $kernel) 17 | { 18 | $kernel->bind('encoding',function (string $outputEncoding,string $inputEncoding = null){ 19 | return EncodeService::convert($this,$outputEncoding,$inputEncoding); 20 | }); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Providers/HttpServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/22 6 | */ 7 | 8 | namespace QL\Providers; 9 | 10 | 11 | use QL\Contracts\ServiceProviderContract; 12 | use QL\Kernel; 13 | use QL\Services\HttpService; 14 | use QL\Services\MultiRequestService; 15 | 16 | class HttpServiceProvider implements ServiceProviderContract 17 | { 18 | public function register(Kernel $kernel) 19 | { 20 | $kernel->bind('get',function (...$args){ 21 | return HttpService::get($this,...$args); 22 | }); 23 | 24 | $kernel->bind('post',function (...$args){ 25 | return HttpService::post($this,...$args); 26 | }); 27 | 28 | $kernel->bind('postJson',function (...$args){ 29 | return HttpService::postJson($this,...$args); 30 | }); 31 | 32 | $kernel->bind('multiGet',function (...$args){ 33 | return new MultiRequestService($this,'get',...$args); 34 | }); 35 | 36 | $kernel->bind('multiPost',function (...$args){ 37 | return new MultiRequestService($this,'post',...$args); 38 | }); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Providers/PluginServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/22 6 | */ 7 | 8 | namespace QL\Providers; 9 | 10 | use QL\Contracts\ServiceProviderContract; 11 | use QL\Kernel; 12 | use QL\Services\PluginService; 13 | 14 | class PluginServiceProvider implements ServiceProviderContract 15 | { 16 | public function register(Kernel $kernel) 17 | { 18 | $kernel->bind('use',function ($plugins,...$opt){ 19 | return PluginService::install($this,$plugins,...$opt); 20 | }); 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/Providers/SystemServiceProvider.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/22 6 | */ 7 | 8 | namespace QL\Providers; 9 | 10 | use QL\Contracts\ServiceProviderContract; 11 | use QL\Kernel; 12 | use Closure; 13 | 14 | class SystemServiceProvider implements ServiceProviderContract 15 | { 16 | public function register(Kernel $kernel) 17 | { 18 | $kernel->bind('html',function (...$args){ 19 | $this->setHtml(...$args); 20 | return $this; 21 | }); 22 | 23 | $kernel->bind('queryData',function (Closure $callback = null){ 24 | return $this->query()->getData($callback)->all(); 25 | }); 26 | 27 | $kernel->bind('pipe',function (Closure $callback = null){ 28 | return $callback($this); 29 | }); 30 | 31 | } 32 | } -------------------------------------------------------------------------------- /src/QueryList.php: -------------------------------------------------------------------------------- 1 | query = new Query($this); 58 | $this->kernel = (new Kernel($this))->bootstrap(); 59 | Config::getInstance()->bootstrap($this); 60 | } 61 | 62 | public function __call($name, $arguments) 63 | { 64 | if(method_exists($this->query,$name)){ 65 | $result = $this->query->$name(...$arguments); 66 | }else{ 67 | $result = $this->kernel->getService($name)->call($this,...$arguments); 68 | } 69 | return $result; 70 | } 71 | 72 | public static function __callStatic($name, $arguments) 73 | { 74 | $instance = new self(); 75 | return $instance->$name(...$arguments); 76 | } 77 | 78 | public function __destruct() 79 | { 80 | $this->destruct(); 81 | } 82 | 83 | /** 84 | * Get the QueryList single instance 85 | * 86 | * @return QueryList 87 | */ 88 | public static function getInstance() 89 | { 90 | self::$instance || self::$instance = new self(); 91 | return self::$instance; 92 | } 93 | 94 | /** 95 | * Get the Config instance 96 | * @return null|Config 97 | */ 98 | public static function config() 99 | { 100 | return Config::getInstance(); 101 | } 102 | 103 | /** 104 | * Destruction of resources 105 | */ 106 | public function destruct() 107 | { 108 | unset($this->query); 109 | unset($this->kernel); 110 | } 111 | 112 | /** 113 | * Destroy all documents 114 | */ 115 | public static function destructDocuments() 116 | { 117 | phpQuery::$documents = []; 118 | } 119 | 120 | /** 121 | * Bind a custom method to the QueryList object 122 | * 123 | * @param string $name Invoking the name 124 | * @param Closure $provide Called method 125 | * @return $this 126 | */ 127 | public function bind(string $name,Closure $provide) 128 | { 129 | $this->kernel->bind($name,$provide); 130 | return $this; 131 | } 132 | 133 | } -------------------------------------------------------------------------------- /src/Services/EncodeService.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/20 6 | * 编码转换服务 7 | */ 8 | 9 | namespace QL\Services; 10 | 11 | use QL\QueryList; 12 | 13 | class EncodeService 14 | { 15 | public static function convert(QueryList $ql,string $outputEncoding,string $inputEncoding = null) 16 | { 17 | $html = $ql->getHtml(); 18 | $inputEncoding || $inputEncoding = self::detect($html); 19 | $html = iconv($inputEncoding,$outputEncoding.'//IGNORE',$html); 20 | $ql->setHtml($html); 21 | return $ql; 22 | } 23 | 24 | /** 25 | * Attempts to detect the encoding 26 | * @param $string 27 | * @return bool|false|mixed|string 28 | */ 29 | public static function detect($string) 30 | { 31 | $charset=mb_detect_encoding($string, array('ASCII', 'GB2312', 'GBK', 'UTF-8'),true); 32 | if(strtolower($charset)=='cp936') 33 | $charset='GBK'; 34 | return $charset; 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/Services/HttpService.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/22 6 | */ 7 | 8 | namespace QL\Services; 9 | 10 | use GuzzleHttp\Cookie\CookieJar; 11 | use Jaeger\GHttp; 12 | use QL\QueryList; 13 | 14 | class HttpService 15 | { 16 | protected static $cookieJar = null; 17 | 18 | public static function getCookieJar() 19 | { 20 | if(self::$cookieJar == null) 21 | { 22 | self::$cookieJar = new CookieJar(); 23 | } 24 | return self::$cookieJar; 25 | } 26 | 27 | public static function get(QueryList $ql,$url,$args = null,$otherArgs = []) 28 | { 29 | $otherArgs = array_merge([ 30 | 'cookies' => self::getCookieJar(), 31 | 'verify' => false 32 | ],$otherArgs); 33 | $html = GHttp::get($url,$args,$otherArgs); 34 | $ql->setHtml($html); 35 | return $ql; 36 | } 37 | 38 | public static function post(QueryList $ql,$url,$args = null,$otherArgs = []) 39 | { 40 | $otherArgs = array_merge([ 41 | 'cookies' => self::getCookieJar(), 42 | 'verify' => false 43 | ],$otherArgs); 44 | $html = GHttp::post($url,$args,$otherArgs); 45 | $ql->setHtml($html); 46 | return $ql; 47 | } 48 | 49 | public static function postJson(QueryList $ql,$url,$args = null,$otherArgs = []) 50 | { 51 | $otherArgs = array_merge([ 52 | 'cookies' => self::getCookieJar(), 53 | 'verify' => false 54 | ],$otherArgs); 55 | $html = GHttp::postJson($url,$args,$otherArgs); 56 | $ql->setHtml($html); 57 | return $ql; 58 | } 59 | } -------------------------------------------------------------------------------- /src/Services/MultiRequestService.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 18/12/10 6 | * Time: 下午7:05 7 | */ 8 | 9 | namespace QL\Services; 10 | 11 | 12 | use Jaeger\GHttp; 13 | use Closure; 14 | use GuzzleHttp\Psr7\Response; 15 | use QL\QueryList; 16 | 17 | /** 18 | * Class MultiRequestService 19 | * @package QL\Services 20 | * 21 | * @method MultiRequestService withHeaders($headers) 22 | * @method MultiRequestService withOptions($options) 23 | * @method MultiRequestService concurrency($concurrency) 24 | */ 25 | class MultiRequestService 26 | { 27 | protected $ql; 28 | protected $multiRequest; 29 | protected $method; 30 | 31 | public function __construct(QueryList $ql,$method,$urls) 32 | { 33 | $this->ql = $ql; 34 | $this->method = $method; 35 | $this->multiRequest = GHttp::multiRequest($urls); 36 | } 37 | 38 | public function __call($name, $arguments) 39 | { 40 | $this->multiRequest = $this->multiRequest->$name(...$arguments); 41 | return $this; 42 | } 43 | 44 | public function success(Closure $success) 45 | { 46 | $this->multiRequest = $this->multiRequest->success(function(Response $response, $index) use($success){ 47 | $this->ql->setHtml((String)$response->getBody()); 48 | $success($this->ql,$response, $index); 49 | }); 50 | return $this; 51 | } 52 | 53 | public function error(Closure $error) 54 | { 55 | $this->multiRequest = $this->multiRequest->error(function($reason, $index) use($error){ 56 | $error($this->ql,$reason, $index); 57 | }); 58 | return $this; 59 | } 60 | 61 | public function send() 62 | { 63 | $this->multiRequest->{$this->method}(); 64 | } 65 | } -------------------------------------------------------------------------------- /src/Services/PluginService.php: -------------------------------------------------------------------------------- 1 | 5 | * Date: 2017/9/22 6 | */ 7 | 8 | namespace QL\Services; 9 | 10 | use QL\QueryList; 11 | 12 | class PluginService 13 | { 14 | public static function install(QueryList $queryList, $plugins, ...$opt) 15 | { 16 | if(is_array($plugins)) 17 | { 18 | foreach ($plugins as $plugin) { 19 | $plugin::install($queryList); 20 | } 21 | }else{ 22 | $plugins::install($queryList,...$opt); 23 | } 24 | return $queryList; 25 | } 26 | } --------------------------------------------------------------------------------