diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 07999ebc4d..440efb6b59 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -132,3 +132,7 @@ jobs: - name: Run Telescope dashboard tests working-directory: src/telescope run: npm test + + - name: Build Telescope dashboard assets + working-directory: src/telescope + run: npm run build diff --git a/_archive/docs/0.3/.vuepress/config.js b/_archive/docs/0.3/.vuepress/config.js deleted file mode 100644 index 44b79440e9..0000000000 --- a/_archive/docs/0.3/.vuepress/config.js +++ /dev/null @@ -1,80 +0,0 @@ -import { defaultTheme } from '@vuepress/theme-default' -import { defineUserConfig } from 'vuepress/cli' -import { viteBundler } from '@vuepress/bundler-vite' -import { mdEnhancePlugin } from "vuepress-plugin-md-enhance" -import { redirectPlugin } from '@vuepress/plugin-redirect' -import { docsearchPlugin } from '@vuepress/plugin-docsearch' -import { sidebarConfig } from './sidebar.js' -import { removeHtmlExtensionPlugin } from 'vuepress-plugin-remove-html-extension' -import { seoPlugin } from '@vuepress/plugin-seo' - -export default defineUserConfig({ - lang: 'en-US', - title: 'Hypervel', - description: 'A Coroutine PHP Framework for Laravel Artisans.', - - ignoreDeadLinks: true, - bundler: viteBundler(), - - plugins: [ - seoPlugin({ - hostname: 'https://hypervel.org', - fallBackImage: 'https://hypervel.org/home.png', - customHead: (head) => { - head.push(['title', {}, 'Hypervel - A Coroutine PHP Framework for Laravel Artisans.']); - }, - ogp: (ogp, page) => ({ - ...ogp, - 'og:title': 'Hypervel - A Coroutine PHP Framework for Laravel Artisans', - 'og:description': "Hypervel is a Laravel-style framework with native coroutine support for ultra-high performance.", - 'twitter:title': 'Hypervel - A Coroutine PHP Framework For Laravel Artisans', - 'twitter:description': "Hypervel is a Laravel-style framework with native coroutine support for ultra-high performance.", - 'twitter:image': 'https://hypervel.org/home.png', - 'twitter:card': 'summary_large_image', - }), - }), - removeHtmlExtensionPlugin(), - mdEnhancePlugin({ - hint: true, - tasklist: true, - include: true, - tabs: true, - align: true, - chart: true, - }), - redirectPlugin({ - config: { - '/docs': '/docs/introduction.html', - }, - }), - docsearchPlugin({ - appId: 'VH8TNYFP5F', - apiKey: '434aa2025192c8d2fb19499cb78e5d48', - indexName: 'hypervel' - }), - ], - - theme: defaultTheme({ - logo: 'icon.svg', - - docsRepo: 'hypervel/hypervel.org', - - docsBranch: 'main/docs', - - navbar: [ - '/', - { - text: 'Documentation', - link: '/docs/introduction', - }, - { - text: 'GitHub', - link: 'https://github.com/hypervel/hypervel', - } - ], - - sidebar: sidebarConfig, - - sidebarDepth: 0, - }), -}) diff --git a/_archive/docs/0.3/.vuepress/public/favicon.ico b/_archive/docs/0.3/.vuepress/public/favicon.ico deleted file mode 100644 index d6ec6e174d..0000000000 Binary files a/_archive/docs/0.3/.vuepress/public/favicon.ico and /dev/null differ diff --git a/_archive/docs/0.3/.vuepress/public/home.png b/_archive/docs/0.3/.vuepress/public/home.png deleted file mode 100644 index 1d942c3dfb..0000000000 Binary files a/_archive/docs/0.3/.vuepress/public/home.png and /dev/null differ diff --git a/_archive/docs/0.3/.vuepress/public/icon.png b/_archive/docs/0.3/.vuepress/public/icon.png deleted file mode 100644 index c44ab7dd95..0000000000 Binary files a/_archive/docs/0.3/.vuepress/public/icon.png and /dev/null differ diff --git a/_archive/docs/0.3/.vuepress/public/icon.svg b/_archive/docs/0.3/.vuepress/public/icon.svg deleted file mode 100644 index 3f3eee7a06..0000000000 --- a/_archive/docs/0.3/.vuepress/public/icon.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/_archive/docs/0.3/.vuepress/public/images/prompts-example.png b/_archive/docs/0.3/.vuepress/public/images/prompts-example.png deleted file mode 100644 index 45b09ae478..0000000000 Binary files a/_archive/docs/0.3/.vuepress/public/images/prompts-example.png and /dev/null differ diff --git a/_archive/docs/0.3/.vuepress/public/images/whoops.png b/_archive/docs/0.3/.vuepress/public/images/whoops.png deleted file mode 100644 index 710f8e893c..0000000000 Binary files a/_archive/docs/0.3/.vuepress/public/images/whoops.png and /dev/null differ diff --git a/_archive/docs/0.3/.vuepress/public/logo.png b/_archive/docs/0.3/.vuepress/public/logo.png deleted file mode 100644 index 3a1d32ecad..0000000000 Binary files a/_archive/docs/0.3/.vuepress/public/logo.png and /dev/null differ diff --git a/_archive/docs/0.3/.vuepress/public/logo.svg b/_archive/docs/0.3/.vuepress/public/logo.svg deleted file mode 100644 index 046e2221d5..0000000000 --- a/_archive/docs/0.3/.vuepress/public/logo.svg +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/_archive/docs/0.3/.vuepress/sidebar.js b/_archive/docs/0.3/.vuepress/sidebar.js deleted file mode 100644 index dc50ece6f3..0000000000 --- a/_archive/docs/0.3/.vuepress/sidebar.js +++ /dev/null @@ -1,370 +0,0 @@ -export const sidebarConfig = ({ - '/docs': [ - { - text: 'Documentation', - prefix: '/docs/', - children: [ - { - text: 'Prologue', - collapsible: true, - children: [ - { - text: 'Contributing Guide', - link: 'contributions', - } - ] - }, - { - text: 'Getting Started', - collapsible: true, - children: [ - { - text: 'Introduction', - link: 'introduction', - }, - { - text: 'Installation', - link: 'installation', - }, - { - text: 'Configuration', - link: 'configuration', - }, - { - text: 'Directory Structure', - link: 'structure', - }, - { - text: 'Deployment', - link: 'deployment', - } - ], - }, - { - text: 'Architecture Concepts', - collapsible: true, - children: [ - { - text: 'Request Lifecycle', - link: 'lifecycle', - }, - { - text: 'Service Container', - link: 'container', - }, - { - text: 'Service Providers', - link: 'providers', - }, - { - text: 'Facades', - link: 'facades', - } - ], - }, - { - text: 'The Basics', - collapsible: true, - children: [ - { - text: 'Routing', - link: 'routing', - }, - { - text: 'Middleware', - link: 'middleware', - }, - { - text: 'CSRF Protection', - link: 'csrf', - }, - { - text: 'Controllers', - link: 'controllers', - }, - { - text: 'Requests', - link: 'requests', - }, - { - text: 'Responses', - link: 'responses', - }, - { - text: 'Views', - link: 'views', - }, - { - text: 'Blade Templates', - link: 'blade', - }, - { - text: 'URL Generation', - link: 'urls', - }, - { - text: 'Session', - link: 'session', - }, - { - text: 'Validation', - link: 'validation', - }, - { - text: 'Error Handling', - link: 'errors', - }, - { - text: 'Logging', - link: 'logging', - } - ], - }, - { - text: 'Digging Deeper', - collapsible: true, - children: [ - { - text: 'Artisan Console', - link: 'artisan', - }, - { - text: 'Broadcasting', - link: 'broadcasting', - }, - { - text: 'Cache', - link: 'cache', - }, - { - text: 'Collections', - link: 'collections', - }, - { - text: 'Context', - link: 'context', - }, - { - text: 'Coroutine', - link: 'coroutine', - }, - { - text: 'Contracts', - link: 'contracts', - }, - { - text: 'Data Objects', - link: 'data-objects', - }, - { - text: 'Events', - link: 'events', - }, - { - text: 'File Storage', - link: 'filesystem', - }, - { - text: 'Helpers', - link: 'helpers', - }, - { - text: 'HTTP Client', - link: 'http-client', - }, - { - text: 'Localization', - link: 'localization', - }, - { - text: 'Mail', - link: 'mail', - }, - { - text: 'Notifications', - link: 'notifications', - }, - { - text: 'Package Development', - link: 'packages', - }, - { - text: 'Package Porting', - link: 'packages-porting', - }, - { - text: 'Processes', - link: 'processes', - }, - { - text: 'Queues', - link: 'queues', - }, - { - text: 'Rate Limiting', - link: 'rate-limiting', - }, - { - text: 'Strings', - link: 'strings', - }, - { - text: 'Task Scheduling', - link: 'scheduling', - }, - ], - }, - { - text: 'Security', - collapsible: true, - children: [ - { - text: 'Authentication', - link: 'authentication', - }, - { - text: 'Authorization', - link: 'authorization', - }, - { - text: 'Encryption', - link: 'encryption', - }, - { - text: 'Hashing', - link: 'hashing', - } - ], - }, - { - text: 'Database', - collapsible: true, - children: [ - { - text: 'Getting Started', - link: 'database', - }, - { - text: 'Query Builder', - link: 'queries', - }, - { - text: 'Pagination', - link: 'pagination', - }, - { - text: 'Migrations', - link: 'migrations', - }, - { - text: 'Seeding', - link: 'seeding', - }, - { - text: 'Redis', - link: 'redis', - } - ], - }, - { - text: 'Eloquent ORM', - collapsible: true, - children: [ - { - text: 'Getting Started', - link: 'eloquent', - }, - { - text: 'Relationships', - link: 'eloquent-relationships', - }, - { - text: 'Collections', - link: 'eloquent-collections', - }, - { - text: 'Mutators / Casts', - link: 'eloquent-mutators', - }, - { - text: 'API Resources', - link: 'eloquent-resources', - }, - { - text: 'Serialization', - link: 'eloquent-serialization', - }, - { - text: 'Factories', - link: 'eloquent-factories', - } - ], - }, - { - text: 'Testing', - collapsible: true, - children: [ - { - text: 'Getting Started', - link: 'testing', - }, - { - text: 'HTTP Tests', - link: 'http-tests', - }, - { - text: 'Console Tests', - link: 'console-tests', - }, - { - text: 'Database', - link: 'database-testing', - }, - { - text: 'Mocking', - link: 'mocking', - }, - { - text: 'Packages Toolkit', - link: 'testbench', - }, - ], - }, - { - text: 'Packages', - collapsible: true, - children: [ - { - text: 'Horizon', - link: 'horizon', - }, - { - text: 'Sanctum', - link: 'sanctum', - }, - { - text: 'Socialite', - link: 'socialite', - }, - { - text: 'Permission', - link: 'permission', - }, - { - text: 'Prompts', - link: 'prompts', - }, - { - text: 'Nested Set', - link: 'nested-set', - }, - { - text: 'Telescope', - link: 'telescope', - } - ], - }, - ], - } - ], -}) \ No newline at end of file diff --git a/_archive/docs/0.3/.vuepress/styles/palette.scss b/_archive/docs/0.3/.vuepress/styles/palette.scss deleted file mode 100644 index 0b94286e3b..0000000000 --- a/_archive/docs/0.3/.vuepress/styles/palette.scss +++ /dev/null @@ -1,182 +0,0 @@ -.vp-hero-description { - max-width: 40rem; -} - -/* Code examples styles */ -.code-examples-table { - display: flex; - width: 100%; - border: 1px solid var(--c-border); - border-radius: 8px; - margin: 2rem 0; - height: 600px; - background: var(--c-bg); -} - -.code-nav { - flex: 0 0 250px; - background: var(--c-bg-light); - border-right: 1px solid var(--c-border); - overflow-y: auto; - - ul { - list-style: none; - padding: 0; - margin: 0; - } - - .category-item { - padding: 0.75rem 1.5rem; - cursor: pointer; - transition: all 0.3s ease; - border-left: 3px solid transparent; - - &:hover { - background: var(--c-bg); - border-left-color: var(--c-brand); - color: var(--c-brand); - } - - &.active { - background: var(--c-bg); - border-left-color: var(--c-brand); - color: var(--c-brand); - } - } -} - -.code-content { - flex: 1; - padding: 1.5rem; - overflow-y: auto; - height: 100%; -} - -.code-block { - display: none; - opacity: 0; - transition: opacity 0.3s ease; - - &.active { - display: block; - opacity: 1; - } - - .file-name { - padding: 0.5rem 1rem; - background: var(--c-bg-light); - color: var(--c-text); - font-family: var(--font-family-mono); - font-size: 0.9rem; - border: 1px solid var(--c-border); - border-bottom: none; - border-top-left-radius: 6px; - border-top-right-radius: 6px; - margin-bottom: 0; - } - - div[class*="language-"] { - margin: 0; - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; - } -} - -/* Responsive design */ -@media (max-width: 768px) { - .code-examples-table { - flex-direction: column; - height: auto; - } - - .code-nav { - flex: none; - height: 150px; - border-right: none; - border-bottom: 1px solid var(--c-border); - } - - .code-content { - flex: none; - height: 250px; - } -} - -/* Benchmark tabs styles */ -.benchmark-tabs { - display: flex; - flex-direction: column; - width: 100%; - border: 1px solid var(--c-border); - border-radius: 8px; - margin: 2rem 0; - background: var(--c-bg); -} - -.benchmark-nav { - flex: 0 0 auto; - background: var(--c-bg-light); - border-bottom: 1px solid var(--c-border); - - ul { - list-style: none; - padding: 0; - margin: 0; - display: flex; - } - - .benchmark-item { - padding: 0.75rem 1.5rem; - cursor: pointer; - transition: all 0.3s ease; - border-bottom: 3px solid transparent; - - &:hover { - background: var(--c-bg); - border-bottom-color: var(--c-brand); - color: var(--c-brand); - } - - &.active { - background: var(--c-bg); - border-bottom-color: var(--c-brand); - color: var(--c-brand); - } - } -} - -.benchmark-content { - flex: 1; - padding: 1.5rem; -} - -.benchmark-block { - display: none; - opacity: 0; - transition: opacity 0.3s ease; - - &.active { - display: block; - opacity: 1; - } -} - -/* Responsive design for benchmark tabs */ -@media (max-width: 768px) { - .benchmark-nav ul { - flex-direction: column; - } - - .benchmark-nav .benchmark-item { - border-bottom: none; - border-left: 3px solid transparent; - - &:hover, - &.active { - border-left-color: var(--c-brand); - border-bottom-color: transparent; - } - } -} \ No newline at end of file diff --git a/_archive/docs/0.3/README.md b/_archive/docs/0.3/README.md deleted file mode 100644 index 9db4f58ad1..0000000000 --- a/_archive/docs/0.3/README.md +++ /dev/null @@ -1,631 +0,0 @@ ---- -home: true -title: Hypervel -heroImage: icon.svg -actions: - - text: Get Started - link: /docs/introduction - type: primary - - text: GitHub - link: https://github.com/hypervel/hypervel - type: secondary -features: - - title: Laravel Friendly - details: By porting Laravel's core infrastructure and fundamental components, this framework enables Laravel developers to quickly adapt to it. - - title: High Performance - details: Leveraging Swoole's native coroutine support, it delivers exceptional performance and efficient concurrency handling. - - title: Ecosystem Compatibility - details: Hypervel is compatible with the Hyperf ecosystem, sharing the same community resources and packages. -footer: MIT Licensed | Copyright © 2024-present Hypervel ---- - -## Coding Like in Laravel - -Laravel artisans can enjoy a familiar development experience that mirrors the original framework. The simple, elegant syntax remains intact, enabling developers to stay productive while leveraging enhanced performance. - -
-
- -
- -
-
- -### Authentication - -Authenticating users is as simple as adding an authentication middleware to your Hypervel route definition: - -```php -Route::get('/profile', ProfileController::class, [ - 'middleware' => 'auth' -]); -``` - -Once the user is authenticated, you can access the authenticated user via the Auth facade: - -```php -use Hypervel\Support\Facades\Auth; - -$user = Auth::user(); -``` - -Of course, you may define your own authentication middleware, allowing you to customize the authentication process. - -[Read Authentication docs](/docs/authentication) - -
- -
- -### Authorization - -You'll often need to check whether an authenticated user is authorized to perform a specific action. Hypervel's model policies make it a breeze: - -```shell:no-line-numbers -php artisan make:policy UserPolicy -``` - -Once you've defined your authorization rules in the generated policy class, you can authorize the user's request in your controller methods: - -```php -public function update(Request $request, Invoice $invoice) -{ - Gate::authorize('update', $invoice); - - $invoice->update(/* ... */); -} -``` - -[Read Authorization docs](/docs/authorization) - -
- -
- -### Eloquent - -Scared of databases? Don't be. Hypervel’s Eloquent ORM makes it painless to interact with your application's data, and models, migrations, and relationships can be quickly scaffolded: - -```shell:no-line-numbers -php artisan make:model Invoice --migration -``` - -Once you've defined your model structure and relationships, you can interact with your database using Eloquent's powerful, expressive syntax: - -```php -// Create a related model... -$user->invoices()->create(['amount' => 100]); - -// Update a model... -$invoice->update(['amount' => 200]); - -// Retrieve models... -$invoices = Invoice::unpaid()->where('amount', '>=', 100)->get(); - -// Rich API for model interactions... -$invoices->each->pay(); -``` - -[Read Eloquent docs](/docs/eloquent) - -
- -
- -### Database Migrations - -Migrations are like version control for your database, allowing your team to define and share your application's database schema definition: - - -```php -return new class extends Migration { - public function up() - { - Schema::create('flights', function (Blueprint $table) { - $table->uuid()->primary(); - $table->foreignUuid('airline_id')->constrained(); - $table->string('name'); - $table->timestamps(); - }); - } -}; -``` - -[Read Migration docs](/docs/migrations) - -
- -
- -### Validation - -Laravel has over 90 powerful, built-in validation rules and, using Hypervel Precognition, can provide live validation on your frontend: - -```php -public function update(Request $request) -{ - $validated = $request->validate([ - 'email' => 'required|email|unique:users', - 'password' => Password::required()->min(8), - ]); - - $request->user()->update($validated); -} -``` - -[Read Validation docs](/docs/validation) - -
- -
- -### Notifications & Mail - -Use Hypervel to quickly send beautifully styled notifications to your users via email, Slack, SMS, in-app, and more: - -```shell:no-line-numbers -php artisan make:notification InvoicePaid -``` - -Once you have generated a notification, you can easily send the message to one of your application's users: - -```php -$user->notify(new InvoicePaid($invoice)); -``` - -[Read Notification and Mail docs](/docs/notifications) - -
- -
- -### File Storage - -Hypervel provides a robust filesystem abstraction layer, providing a single, unified API for interacting with local filesystems and cloud based filesystems like Amazon S3: - -```php -$path = Storage::disk('s3') - ->put('avatars/1', $request->file('avatar')); -``` - -Regardless of where your files are stored, interact with them using Hypervel's simple, elegant syntax: - -```php -$content = Storage::get('photo.jpg'); - -Storage::put('photo.jpg', $content); -``` - -[Read File Storage docs](/docs/filesystem) - -
- -
- -### Job Queues - -Hypervel lets you to offload slow jobs to a background queue, keeping your web requests snappy: - -```php -$podcast = Podcast::create(/* ... */); - -ProcessPodcast::dispatch($podcast)->onQueue('podcasts'); -``` - -You can run as many queue workers as you need to handle your workload: - -```shell:no-line-numbers -php artisan queue:work redis --queue=podcasts -``` - -[Read Queues docs](/docs/queues) - -
- -
- -### Task Scheduling - -Schedule recurring jobs and commands with an expressive syntax and say goodbye to complicated configuration files: - -```php -$schedule->job(NotifySubscribers::class)->hourly(); -``` - -Hypervel's scheduler can even handle multiple servers and offers built-in overlap prevention: - -```php -$schedule->job(NotifySubscribers::class) - ->dailyAt('9:00') - ->onOneServer() - ->withoutOverlapping(); -``` - -[Read Task Scheduling docs](/docs/scheduling) - -
- -
- -### Testing - -Hypervel is built for testing. From unit tests to feature tests, you’ll feel more confident in deploying your application: - -```php -class RefreshDatabaseTest extends TestCase -{ - use RefreshDatabase; - - public function testCreateUser() - { - $user = User::factory()->create(); - - $this->assertDatabaseHas('users', [ - 'id' => $user->id, - ]); - } -} -``` - -[Read Testing docs](/docs/testing) - -
- -
- -### Events & Websockets - -Hypervel's events allow you to send and listen for events across your application, and listeners can easily be dispatched to a background queue: - -```php -OrderShipped::dispatch($order); -``` - -```php -class SendShipmentNotification implements ShouldQueue -{ - public function handle(OrderShipped $event): void - { - // ... - } -} -``` - -Your frontend application can even subscribe to your Hypervel events using [Laravel Echo](/docs/broadcasting) and WebSockets, allowing you to build real-time, dynamic applications: - -```js -Echo.private(`orders.${orderId}`) - .listen('OrderShipped', (e) => { - console.log(e.order); - }); -``` - -[Read Events docs](/docs/events) - -
- -
-
- -## Blazing Fast - -The concurrency capability of Hypervel is at least ten times better than traditional Laravel Octane and also offers lower response latency, which makes it an excellent choice for building web applications in high concurrency use cases. - -> For more detailed benchmark, please see [here](/docs/introduction#benchmark). - -
-
- -
- -
-
- -* Laravel Octane (8 workers) -```text:no-line-numbers -Running 10s test @ http://127.0.0.1:8000/api - 4 threads and 100 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 15.93ms 16.86ms 155.82ms 87.02% - Req/Sec 2.07k 420.46 3.10k 66.00% - 82661 requests in 10.04s, 16.95MB read -Requests/sec: 8230.97 -Transfer/sec: 1.69MB -``` - -* Hypervel (8 workers) - -```text:no-line-numbers -Running 10s test @ http://127.0.0.1:9501/api - 4 threads and 100 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 7.66ms 17.85ms 249.92ms 90.25% - Req/Sec 24.42k 10.47k 54.37k 68.53% - 971692 requests in 10.06s, 151.98MB read -Requests/sec: 96562.80 -Transfer/sec: 15.10MB -``` - -::: chart - -```json -{ - "type": "bar", - "data": { - "labels": ["Simple API Test"], - "datasets": [ - { - "label": "Laravel Octane", - "data": [8230.97], - "backgroundColor": "rgba(255, 182, 193, 0.8)", - "borderColor": "rgba(255, 182, 193, 1)", - "borderWidth": 1 - }, - { - "label": "Hypervel", - "data": [96562.80], - "backgroundColor": "rgba(173, 216, 230, 0.8)", - "borderColor": "rgba(173, 216, 230, 1)", - "borderWidth": 1 - } - ] - }, - "options": { - "responsive": true, - "scales": { - "x": { - "stacked": false - }, - "y": { - "stacked": false, - "title": { - "display": true, - "text": "QPS" - } - } - }, - "plugins": { - "title": { - "display": true, - "text": "Laravel Octane vs Hypervel - Simple API Test" - }, - "legend": { - "position": "top" - } - } - } -} -``` -::: - -
- -
- -* Laravel Octane (8 workers) -```text:no-line-numbers -Running 10s test @ http://127.0.0.1:8000/api - 4 threads and 100 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 1.03s 184.92us 1.03s 87.50% - Req/Sec 1.52 1.29 5.00 54.84% - 80 requests in 10.10s, 16.80KB read - Socket errors: connect 0, read 0, write 0, timeout 72 -Requests/sec: 7.92 -Transfer/sec: 1.66KB -``` - -* Hypervel (8 workers) -```text:no-line-numbers -Running 10s test @ http://10.10.4.12:9501/api - 16 threads and 15000 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 1.02s 64.72ms 1.87s 93.62% - Req/Sec 1.16k 1.68k 9.15k 87.59% - 109401 requests in 10.09s, 19.82MB read -Requests/sec: 10842.71 -Transfer/sec: 1.96MB -``` - -::: chart - -```json -{ - "type": "bar", - "data": { - "labels": ["Simulated I/O Wait Test"], - "datasets": [ - { - "label": "Laravel Octane", - "data": [7.92], - "backgroundColor": "rgba(255, 182, 193, 0.8)", - "borderColor": "rgba(255, 182, 193, 1)", - "borderWidth": 1 - }, - { - "label": "Hypervel", - "data": [10842.71], - "backgroundColor": "rgba(173, 216, 230, 0.8)", - "borderColor": "rgba(173, 216, 230, 1)", - "borderWidth": 1 - } - ] - }, - "options": { - "responsive": true, - "scales": { - "x": { - "stacked": false - }, - "y": { - "stacked": false, - "title": { - "display": true, - "text": "QPS" - } - } - }, - "plugins": { - "title": { - "display": true, - "text": "Laravel Octane vs Hypervel - Simulated I/O Wait Test" - }, - "legend": { - "position": "top" - } - } - } -} -``` -::: - -
- -
-
- -## Native Coroutine Support - -Unlike other async libraries such as AMPHP or ReactPHP, where developers must rewrite every blocking I/O client in their applications to avoid blocking the event loop, this means you can't utilize the current PHP ecosystem, such as Guzzle, PDO, Redis clients, or other native PHP functions. - -> See: [How can I use blocking functions?](https://github.com/reactphp/reactphp/wiki/FAQ#how-can-i-use-blocking-functions) in ReactPHP. - -All components in Hypervel support coroutines out of the box. Even better, Hypervel can seamlessly transform PHP's built-in blocking I/O functions into coroutines, thanks to [Swoole's runtime hooks](https://wiki.swoole.com/en/#/runtime?id=runtime). - -```php -use Hypervel\Coroutine\Coroutine; -use Hypervel\Support\Facades\Http; - -Coroutine::create(function () { - // It won't block the main process - $response = Http::get('https://hypervel.org'); - // Even built-in blocking functions will become coroutines - sleep(1); - echo $response->body(); -}); - -// This line will be printed first -echo 'Hello world!' . PHP_EOL; -``` - -> See [Coroutine](/docs/coroutine) for more detailed information. - -## Frequently Asked Questions - -
-

Is Hypervel compatible with Laravel packages?

-

No! While Hypervel maintains the similar development experience like Laravel, unfortunately, you can't install Laravel packages on this framework due to the fundamental differences in architecture. Developers need to migrate these Laravel packages on their own. We encourage developers to contribute to Hypervel's ecosystem together.

-
- -
-

How does Hypervel achieve high performance?

-

Hypervel achieves high performance through Swoole's coroutine system, which enables non-blocking I/O operations and efficient concurrency handling. This allows the framework to handle more concurrent connections with fewer resources compared to traditional PHP-FPM.

-
- -
-

Why not use Octane directly?

-

Octane accelerates Laravel by maintaining the framework in a persistent application state, resetting request-scoped resources between requests. However, it doesn't introduce non-blocking I/O capabilities to Laravel's architecture. Furthermore, all components in Laravel weren't designed with coroutines in mind. It will cause states bleeding while context switching among coroutines. - -For I/O-intensive scenarios, even with Octane's improvements, your application's ability to handle concurrent requests is still limited by the duration of these I/O operations. -

-
- -
-

Should I migrate my current Laravel projects to Hypervel?

-

If you don't have performance issues in your current projects, I would suggest remaining in Laravel because of the mature community and rich ecosystem. There will be necessary refactorings for the migration process. You can also consider migrating only the APIs and modules with performance issues to Hypervel, and adopt architecture like API Gateway to allow Laravel and Hypervel projects to work simultaneously.

-
- - diff --git a/_archive/docs/0.3/docs/artisan.md b/_archive/docs/0.3/docs/artisan.md deleted file mode 100644 index abaf4a6c1b..0000000000 --- a/_archive/docs/0.3/docs/artisan.md +++ /dev/null @@ -1,629 +0,0 @@ -# Artisan Console -[[toc]] - -## Introduction - -Artisan is the command line interface included with Hypervel. Artisan exists at the root of your application as the `artisan` script and provides a number of helpful commands that can assist you while you build your application. To view a list of all available Artisan commands, you may use the `list` command: - -```shell:no-line-numbers -php artisan list -``` - -Every command also includes a "help" screen which displays and describes the command's available arguments and options. To view a help screen, precede the name of the command with `help`: - -```shell:no-line-numbers -php artisan help migrate -``` - -### Tinker (REPL) - -Hypervel Tinker is a powerful REPL for the Hypervel framework. Powered by the [PsySH](https://github.com/bobthecow/psysh) package. - -#### Installation - -All Hypervel applications include Tinker by default. However, you may install Tinker using Composer if you have previously removed it from your application: - -```shell:no-line-numbers -composer require friendsofhyperf/tinker -``` - -#### Usage - -Tinker allows you to interact with your entire Hypervel application on the command line, including your Eloquent models, jobs, events, and more. To enter the Tinker environment, run the `tinker` Artisan command: - -```shell:no-line-numbers -php artisan tinker -``` - -You can publish Tinker's configuration file using the `vendor:publish` command: - -```shell:no-line-numbers -php artisan vendor:publish friendsofhyperf/tinker -``` - -#### Command Allow List - -Tinker utilizes an "allow" list to determine which Artisan commands are allowed to be run within its shell. By default, you may run the `migrate`command. If you would like to allow more commands you may add them to the `commands` array in your `tinker.php` configuration file: - -```php -'commands' => [ - // App\Console\Commands\ExampleCommand::class, -], -``` - -#### Classes That Should Not Be Aliased - -Typically, Tinker automatically aliases classes as you interact with them in Tinker. However, you may wish to never alias some classes. You may accomplish this by listing the classes in the `dont_alias` array of your `tinker.php` configuration file: - -```php -'dont_alias' => [ - App\Models\User::class, -], -``` - -## Writing Commands - -In addition to the commands provided with Artisan, you may build your own custom commands. Commands are typically stored in the `app/Console/Commands` directory; however, you are free to choose your own storage location as long as your commands can be loaded by Composer. - -### Generating Commands - -To create a new command, you may use the `make:command` Artisan command. This command will create a new command class in the `app/Console/Commands` directory. Don't worry if this directory does not exist in your application - it will be created the first time you run the `make:command` Artisan command: - -```shell:no-line-numbers -php artisan make:command SendEmails -``` - -### Command Structure - -After generating your command, you should define appropriate values for the `signature` and `description` properties of the class. These properties will be used when displaying your command on the `list` screen. The `signature` property also allows you to define [your command's input expectations](#defining-input-expectations). The `handle` method will be called when your command is executed. You may place your command logic in this method. - -Let's take a look at an example command. Note that we are able to request any dependencies we need via the command's `handle` method. The Hypervel [service container](/docs/container) will automatically inject all dependencies that are type-hinted in this method's signature: - -```php -send(User::find($this->argument('user'))); - } -} -``` - -::: tip -For greater code reuse, it is good practice to keep your console commands light and let them defer to application services to accomplish their tasks. In the example above, note that we inject a service class to do the "heavy lifting" of sending the e-mails. -::: - -### Closure Commands - -Closure based commands provide an alternative to defining console commands as classes. In the same way that route closures are an alternative to controllers, think of command closures as an alternative to command classes. Within the `commands` method of your `app/Console/Kernel.php` file, Hypervel loads the `routes/console.php` file: - -```php -/** - * Register the closure based commands for the application. - */ -protected function commands(): void -{ - require base_path('routes/console.php'); -} -``` - -Even though this file does not define HTTP routes, it defines console based entry points (routes) into your application. Within this file, you may define all of your closure based console commands using the `Artisan::command` method. The `command` method accepts two arguments: the [command signature](#defining-input-expectations) and a closure which receives the command's arguments and options: - -```php -Artisan::command('mail:send {user}', function (string $user) { - $this->info("Sending email to: {$user}!"); -}); -``` - -The closure is bound to the underlying command instance, so you have full access to all of the helper methods you would typically be able to access on a full command class. - -#### Type-Hinting Dependencies - -In addition to receiving your command's arguments and options, command closures may also type-hint additional dependencies that you would like resolved out of the [service container](/docs/container): - -```php -use App\Models\User; -use App\Support\DripEmailer; - -Artisan::command('mail:send {user}', function (DripEmailer $drip, string $user) { - $drip->send(User::find($user)); -}); -``` - -#### Closure Command Descriptions - -When defining a closure based command, you may use the `describe` method to add a description to the command. This description will be displayed when you run the `php artisan list` or `php artisan help` commands: - -```php -Artisan::command('mail:send {user}', function (string $user) { - // ... -})->describe('Send a marketing email to a user'); -``` - -## Defining Input Expectations - -When writing console commands, it is common to gather input from the user through arguments or options. Hypervel makes it very convenient to define the input you expect from the user using the `signature` property on your commands. The `signature` property allows you to define the name, arguments, and options for the command in a single, expressive, route-like syntax. - -### Arguments - -All user supplied arguments and options are wrapped in curly braces. In the following example, the command defines one required argument: `user`: - -```php -/** - * The name and signature of the console command. - * - * @var null|string - */ -protected ?string $signature = 'mail:send {user}'; -``` - -You may also make arguments optional or define default values for arguments: - -```php -// Optional argument... -'mail:send {user?}' - -// Optional argument with default value... -'mail:send {user=foo}' -``` - -### Options - -Options, like arguments, are another form of user input. Options are prefixed by two hyphens (`--`) when they are provided via the command line. There are two types of options: those that receive a value and those that don't. Options that don't receive a value serve as a boolean "switch". Let's take a look at an example of this type of option: - -```php -/** - * The name and signature of the console command. - * - * @var null|string - */ -protected ?string $signature = 'mail:send {user} {--queue}'; -``` - -In this example, the `--queue` switch may be specified when calling the Artisan command. If the `--queue` switch is passed, the value of the option will be `true`. Otherwise, the value will be `false`: - -```shell:no-line-numbers -php artisan mail:send 1 --queue -``` - -#### Options With Values - -Next, let's take a look at an option that expects a value. If the user must specify a value for an option, you should suffix the option name with a `=` sign: - -```php -/** - * The name and signature of the console command. - * - * @var null|string - */ -protected ?string $signature = 'mail:send {user} {--queue=}'; -``` - -In this example, the user may pass a value for the option like so. If the option is not specified when invoking the command, its value will be `null`: - -```shell:no-line-numbers -php artisan mail:send 1 --queue=default -``` - -You may assign default values to options by specifying the default value after the option name. If no option value is passed by the user, the default value will be used: - -```php -'mail:send {user} {--queue=default}' -``` - -#### Option Shortcuts - -To assign a shortcut when defining an option, you may specify it before the option name and use the `|` character as a delimiter to separate the shortcut from the full option name: - -```php -'mail:send {user} {--Q|queue}' -``` - -When invoking the command on your terminal, option shortcuts should be prefixed with a single hyphen and no `=` character should be included when specifying a value for the option: - -```shell:no-line-numbers -php artisan mail:send 1 -Qdefault -``` - -### Input Arrays - -If you would like to define arguments or options to expect multiple input values, you may use the `*` character. First, let's take a look at an example that specifies such an argument: - -```php -'mail:send {user*}' -``` - -When calling this method, the `user` arguments may be passed in order to the command line. For example, the following command will set the value of `user` to an array with `1` and `2` as its values: - -```shell:no-line-numbers -php artisan mail:send 1 2 -``` - -This `*` character can be combined with an optional argument definition to allow zero or more instances of an argument: - -```php -'mail:send {user?*}' -``` - -#### Option Arrays - -When defining an option that expects multiple input values, each option value passed to the command should be prefixed with the option name: - -```php -'mail:send {--id=*}' -``` - -Such a command may be invoked by passing multiple `--id` arguments: - -```shell:no-line-numbers -php artisan mail:send --id=1 --id=2 -``` - -### Input Descriptions - -You may assign descriptions to input arguments and options by separating the argument name from the description using a colon. If you need a little extra room to define your command, feel free to spread the definition across multiple lines: - -```php -/** - * The name and signature of the console command. - */ -protected ?string $signature = 'mail:send - {user : The ID of the user} - {--queue : Whether the job should be queued}'; -``` - -## Command I/O - -### Retrieving Input - -While your command is executing, you will likely need to access the values for the arguments and options accepted by your command. To do so, you may use the `argument` and `option` methods. If an argument or option does not exist, `null` will be returned: - -```php -/** - * Execute the console command. - */ -public function handle(): void -{ - $userId = $this->argument('user'); -} -``` - -If you need to retrieve all of the arguments as an `array`, call the `arguments` method: - -```php -$arguments = $this->arguments(); -``` - -Options may be retrieved just as easily as arguments using the `option` method. To retrieve all of the options as an array, call the `options` method: - -```php -// Retrieve a specific option... -$queueName = $this->option('queue'); - -// Retrieve all options as an array... -$options = $this->options(); -``` - -### Prompting for Input - -In addition to displaying output, you may also ask the user to provide input during the execution of your command. The `ask` method will prompt the user with the given question, accept their input, and then return the user's input back to your command: - -```php -/** - * Execute the console command. - */ -public function handle(): void -{ - $name = $this->ask('What is your name?'); - - // ... -} -``` - -The `ask` method also accepts an optional second argument which specifies the default value that should be returned if no user input is provided: - -```php -$name = $this->ask('What is your name?', 'Taylor'); -``` - -The `secret` method is similar to `ask`, but the user's input will not be visible to them as they type in the console. This method is useful when asking for sensitive information such as passwords: - -```php -$password = $this->secret('What is the password?'); -``` - -#### Asking for Confirmation - -If you need to ask the user for a simple "yes or no" confirmation, you may use the `confirm` method. By default, this method will return `false`. However, if the user enters `y` or `yes` in response to the prompt, the method will return `true`. - -```php -if ($this->confirm('Do you wish to continue?')) { - // ... -} -``` - -If necessary, you may specify that the confirmation prompt should return `true` by default by passing `true` as the second argument to the `confirm` method: - -```php -if ($this->confirm('Do you wish to continue?', true)) { - // ... -} -``` - -#### Auto-Completion - -The `anticipate` method can be used to provide auto-completion for possible choices. The user can still provide any answer, regardless of the auto-completion hints: - -```php -$name = $this->anticipate('What is your name?', ['Taylor', 'Dayle']); -``` - -Alternatively, you may pass a closure as the second argument to the `anticipate` method. The closure will be called each time the user types an input character. The closure should accept a string parameter containing the user's input so far, and return an array of options for auto-completion: - -```php -$name = $this->anticipate('What is your address?', function (string $input) { - // Return auto-completion options... -}); -``` - -#### Multiple Choice Questions - -If you need to give the user a predefined set of choices when asking a question, you may use the `choice` method. You may set the array index of the default value to be returned if no option is chosen by passing the index as the third argument to the method: - -```php -$name = $this->choice( - 'What is your name?', - ['Taylor', 'Dayle'], - $defaultIndex -); -``` - -In addition, the `choice` method accepts optional fourth and fifth arguments for determining the maximum number of attempts to select a valid response and whether multiple selections are permitted: - -```php -$name = $this->choice( - 'What is your name?', - ['Taylor', 'Dayle'], - $defaultIndex, - $maxAttempts = null, - $allowMultipleSelections = false -); -``` - -### Writing Output - -To send output to the console, you may use the `line`, `info`, `comment`, `question`, `warn`, and `error` methods. Each of these methods will use appropriate ANSI colors for their purpose. For example, let's display some general information to the user. Typically, the `info` method will display in the console as green colored text: - -```php -/** - * Execute the console command. - */ -public function handle(): void -{ - // ... - - $this->info('The command was successful!'); -} -``` - -To display an error message, use the `error` method. Error message text is typically displayed in red: - -```php -$this->error('Something went wrong!'); -``` - -You may use the `line` method to display plain, uncolored text: - -```php -$this->line('Display this on the screen'); -``` - -You may use the `newLine` method to display a blank line: - -```php -// Write a single blank line... -$this->newLine(); - -// Write three blank lines... -$this->newLine(3); -``` - -#### Tables - -The `table` method makes it easy to correctly format multiple rows / columns of data. All you need to do is provide the column names and the data for the table and Hypervel will -automatically calculate the appropriate width and height of the table for you: - -```php -use App\Models\User; - -$this->table( - ['Name', 'Email'], - User::all(['name', 'email'])->toArray() -); -``` - -#### Progress Bars - -For long running tasks, it can be helpful to show a progress bar that informs users how complete the task is. Using the `withProgressBar` method, Hypervel will display a progress bar and advance its progress for each iteration over a given iterable value: - -```php -use App\Models\User; - -$users = $this->withProgressBar(User::all(), function (User $user) { - $this->performTask($user); -}); -``` - -Sometimes, you may need more manual control over how a progress bar is advanced. First, define the total number of steps the process will iterate through. Then, advance the progress bar after processing each item: - -```php -$users = App\Models\User::all(); - -$bar = $this->output->createProgressBar(count($users)); - -$bar->start(); - -foreach ($users as $user) { - $this->performTask($user); - - $bar->advance(); -} - -$bar->finish(); -``` - -::: note -For more advanced options, check out the [Symfony Progress Bar component documentation](https://symfony.com/doc/current/components/console/helpers/progressbar). -::: - -## Registering Commands - -All of your console commands are registered within your application's `App\Console\Kernel` class, which is your application's "console kernel". Within the `commands` method of this class, you will see a call to the kernel's `load` method. The `load` method will scan the `app/Console/Commands` directory and automatically register each command it contains with Artisan. You are even free to make additional calls to the `load` method to scan other directories for Artisan commands: - -```php -/** - * Register the commands for the application. - */ -protected function commands(): void -{ - $this->load(__DIR__.'/Commands'); - $this->load(__DIR__.'/../Domain/Orders/Commands'); - - // ... -} -``` - -If necessary, you may manually register commands by adding the command's class name to a `$commands` property within your `App\Console\Kernel` class. If this property is not already defined on your kernel, you should define it manually. When Artisan boots, all the commands listed in this property will be resolved by the [service container](/docs/container) and registered with Artisan: - -```php -protected array $commands = [ - Commands\SendEmails::class -]; -``` - -## Programmatically Executing Commands - -Sometimes you may wish to execute an Artisan command outside of the CLI. For example, you may wish to execute an Artisan command from a route or controller. You may use the `call` method on the `Artisan` facade to accomplish this. The `call` method accepts either the command's signature name or class name as its first argument, and an array of command parameters as the second argument. The exit code will be returned: - -```php -use Hypervel\Support\Facades\Artisan; - -Route::post('/user/{user}/mail', function (string $user) { - $exitCode = Artisan::call('mail:send', [ - 'user' => $user, '--queue' => 'default' - ]); - - // ... -}); -``` - -Alternatively, you may pass the entire Artisan command to the `call` method as a string: - -```php -Artisan::call('mail:send 1 --queue=default'); -``` - -#### Passing Array Values - -If your command defines an option that accepts an array, you may pass an array of values to that option: - -```php -use Hypervel\Support\Facades\Artisan; - -Route::post('/mail', function () { - $exitCode = Artisan::call('mail:send', [ - '--id' => [5, 13] - ]); -}); -``` - -#### Passing Boolean Values - -If you need to specify the value of an option that does not accept string values, such as the `--force` flag on the `migrate:refresh` command, you should pass `true` or `false` as the value of the option: - -```php -$exitCode = Artisan::call('migrate:refresh', [ - '--force' => true, -]); -``` - -### Calling Commands From Other Commands - -Sometimes you may wish to call other commands from an existing Artisan command. You may do so using the `call` method. This `call` method accepts the command name and an array of command arguments / options: - -```php -/** - * Execute the console command. - */ -public function handle(): void -{ - $this->call('mail:send', [ - 'user' => 1, '--queue' => 'default' - ]); - - // ... -} -``` - -If you would like to call another console command and suppress all of its output, you may use the `callSilently` method. The `callSilently` method has the same signature as the `call` method: - -```php -$this->callSilently('mail:send', [ - 'user' => 1, '--queue' => 'default' -]); -``` - -## Signal Handling - -As you may know, operating systems allow signals to be sent to running processes. For example, the `SIGTERM` signal is how operating systems ask a program to terminate. If you wish to listen for signals in your Artisan console commands and execute code when they occur, you may use the `trap` method: - -```php -/** - * Execute the console command. - */ -public function handle(): void -{ - $this->trap(SIGTERM, fn () => $this->shouldKeepRunning = false); - - while ($this->shouldKeepRunning) { - // ... - } -} -``` - -To listen for multiple signals at once, you may provide an array of signals to the `trap` method: - -```php -$this->trap([SIGTERM, SIGQUIT], function (int $signal) { - $this->shouldKeepRunning = false; - - dump($signal); // SIGTERM / SIGQUIT -}); -``` - -## Events - -Artisan dispatches three events when running commands: `Hyperf\Command\Event\BeforeHandle`, `Hyperf\Command\Event\AfterHandle`, `Hyperf\Command\Event\AfterExecute` and `Hyperf\Command\Event\FailToHandle`. - -The `BeforeHandle` event is dispatched immediately when Artisan starts running. Next, the `AfterExecute` event is dispatched immediately after a command runs. If an exception happened during the execution, the `FailToHandle` will be dispatched. Finally, the `AfterExecute` event is dispatched once a command finishes executing. \ No newline at end of file diff --git a/_archive/docs/0.3/docs/authentication.md b/_archive/docs/0.3/docs/authentication.md deleted file mode 100644 index c07a1cfa23..0000000000 --- a/_archive/docs/0.3/docs/authentication.md +++ /dev/null @@ -1,493 +0,0 @@ -# Authentication -[[toc]] - -## Introduction - -Many web applications provide a way for their users to authenticate with the application and "login". Implementing this feature in web applications can be a complex and potentially risky endeavor. For this reason, Hypervel strives to give you the tools you need to implement authentication quickly, securely, and easily. - -At its core, Hypervel's authentication facilities are made up of "guards" and "providers". Guards define how users are authenticated for each request. For example, Hypervel ships with a `session` guard which maintains state using session storage and cookies. - -Providers define how users are retrieved from your persistent storage. Hypervel ships with support for retrieving users using [Eloquent](/docs/eloquent) and the database query builder. However, you are free to define additional providers as needed for your application. - -Your application's authentication configuration file is located at `config/auth.php`. This file contains several well-documented options for tweaking the behavior of Hypervel's authentication services. - -::: note -Guards and providers should not be confused with "roles" and "permissions". To learn more about authorizing user actions via permissions, please refer to the [authorization](/docs/authorization) documentation. -::: - -### Database Considerations - -By default, Hypervel includes an `App\Models\User` [Eloquent model](/docs/eloquent) in your `app/Models` directory. This model may be used with the default Eloquent authentication driver. - -If your application is not using Eloquent, you may use the `database` authentication provider which uses the Hypervel query builder. - -When building the database schema for the `App\Models\User` model, make sure the password column is at least 60 characters in length. Of course, the `users` table migration that is included in new Hypervel applications already creates a column that exceeds this length. - -> `remember_token` is not implemented in Hypervel by default. - -## Authentication Quickstart - -### Retrieving the Authenticated User - -You will often need to interact with the currently authenticated user. While handling an incoming request, you may access the authenticated user via the `Auth` facade's `user` method: - -```php -use Hypervel\Support\Facades\Auth; - -// Retrieve the currently authenticated user... -$user = Auth::user(); - -// Retrieve the currently authenticated user's ID... -$id = Auth::id(); -``` - -Alternatively, once a user is authenticated, you may access the authenticated user via an `Hypervel\Http\Request` instance. Remember, type-hinted classes will automatically be injected into your controller methods. By type-hinting the `Hypervel\Http\Request` object, you may gain convenient access to the authenticated user from any controller method in your application via the request's `user` method: - -```php -user(); - - // ... - - return redirect('/flights'); - } -} -``` - -#### Determining if the Current User is Authenticated - -To determine if the user making the incoming HTTP request is authenticated, you may use the `check` method on the `Auth` facade. This method will return `true` if the user is authenticated: - -```php -use Hypervel\Support\Facades\Auth; - -if (Auth::check()) { - // The user is logged in... -} -``` - -::: note -Even though it is possible to determine if a user is authenticated using the `check` method, you will typically use a middleware to verify that the user is authenticated before allowing the user access to certain routes / controllers. To learn more about this, check out the documentation on [protecting routes](/docs/authentication#protecting-routes). -::: - -### Protecting Routes - -[Route middleware](/docs/middleware) can be used to only allow authenticated users to access a given route. Hypervel ships with an `auth` middleware, which is a [middleware alias](/docs/middleware#middleware-aliases) for the `Hypervel\Auth\Middleware\Authenticate` class. Since this middleware is already aliased internally by Hypervel, all you need to do is attach the middleware to a route definition: - -```php -Route::get('/flights', function () { - // Only authenticated users may access this route... -}, ['middleware' => 'auth']); -``` - -#### Handling Unauthenticated Users - -When the `auth` middleware detects an unauthenticated user, it will throw `Hypervel\Auth\AuthenticationException`. You may override `unauthenticated` function in your exception handler and customize your next steps, such as redirecting user to another route. - -#### Specifying a Guard - -When attaching the `auth` middleware to a route, you may also specify which "guard" should be used to authenticate the user. The guard specified should correspond to one of the keys in the `guards` array of your `auth.php` configuration file: - -```php -Route::get('/flights', function () { - // Only authenticated users may access this route... -}, ['middleware' => 'auth:admin']); -``` - -## Manually Authenticating Users - -We will access Hypervel's authentication services via the `Auth` [facade](/docs/facades), so we'll need to make sure to import the `Auth` facade at the top of the class. Next, let's check out the `attempt` method. The `attempt` method is normally used to handle authentication attempts from your application's "login" form. If authentication is successful, you should regenerate the user's [session](/docs/session) to prevent [session fixation](https://en.wikipedia.org/wiki/Session_fixation): - -```php -validate([ - 'email' => ['required', 'email'], - 'password' => ['required'], - ]); - - if (Auth::attempt($credentials)) { - $request->session()->regenerate(); - - return redirect('/dashboard'); - } - - return redirect( - url()->previous() - ); - } -} -``` - -The `attempt` method accepts an array of key / value pairs as its first argument. The values in the array will be used to find the user in your database table. So, in the example above, the user will be retrieved by the value of the `email` column. If the user is found, the hashed password stored in the database will be compared with the `password` value passed to the method via the array. You should not hash the incoming request's `password` value, since the framework will automatically hash the value before comparing it to the hashed password in the database. An authenticated session will be started for the user if the two hashed passwords match. - -Remember, Hypervel's authentication services will retrieve users from your database based on your authentication guard's "provider" configuration. In the default `config/auth.php` configuration file, the Eloquent user provider is specified and it is instructed to use the `App\Models\User` model when retrieving users. You may change these values within your configuration file based on the needs of your application. - -The `attempt` method will return `true` if authentication was successful. Otherwise, `false` will be returned. - -#### Specifying Additional Conditions - -If you wish, you may also add extra query conditions to the authentication query in addition to the user's email and password. To accomplish this, we may simply add the query conditions to the array passed to the `attempt` method. For example, we may verify that the user is marked as "active": - -```php -if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) { - // Authentication was successful... -} -``` - -For complex query conditions, you may provide a closure in your array of credentials. This closure will be invoked with the query instance, allowing you to customize the query based on your application's needs: - -```php -use Hyperf\Database\Model\Builder; - -if (Auth::attempt([ - 'email' => $email, - 'password' => $password, - fn (Builder $query) => $query->has('activeSubscription'), -])) { - // Authentication was successful... -} -``` - -::: warning -In these examples, `email` is not a required option, it is merely used as an example. You should use whatever column name corresponds to a "username" in your database table. -::: - -The `attemptWhen` method, which receives a closure as its second argument, may be used to perform more extensive inspection of the potential user before actually authenticating the user. The closure receives the potential user and should return `true` or `false` to indicate if the user may be authenticated: - -```php -if (Auth::attemptWhen([ - 'email' => $email, - 'password' => $password, -], function (User $user) { - return $user->isNotBanned(); -})) { - // Authentication was successful... -} -``` - -#### Accessing Specific Guard Instances - -Via the `Auth` facade's `guard` method, you may specify which guard instance you would like to utilize when authenticating the user. This allows you to manage authentication for separate parts of your application using entirely separate authenticatable models or user tables. - -The guard name passed to the `guard` method should correspond to one of the guards configured in your `auth.php` configuration file: - -```php -if (Auth::guard('admin')->attempt($credentials)) { - // ... -} -``` - -### Other Authentication Methods - -#### Authenticate a User Instance - -If you need to set an existing user instance as the currently authenticated user, you may pass the user instance to the `Auth` facade's `login` method. The given user instance must be an implementation of the `Hypervel\Auth\Contracts\Authenticatable` [contract](/docs/contracts). The `App\Models\User` model included with Hypervel already implements this interface. This method of authentication is useful when you already have a valid user instance, such as directly after a user registers with your application: - -```php -use Hypervel\Support\Facades\Auth; - -Auth::login($user); -``` - -You may pass a boolean value as the second argument to the `login` method. This value indicates if "remember me" functionality is desired for the authenticated session. Remember, this means that the session will be authenticated indefinitely or until the user manually logs out of the application: - -```php -Auth::login($user, $remember = true); -``` - -If needed, you may specify an authentication guard before calling the `login` method: - -```php -Auth::guard('admin')->login($user); -``` - -#### Authenticate a User by ID - -To authenticate a user using their database record's primary key, you may use the `loginUsingId` method. This method accepts the primary key of the user you wish to authenticate: - -```php -Auth::loginUsingId(1); -``` - -#### Authenticate a User Once - -You may use the `once` method to authenticate a user with the application for a single request. No sessions or cookies will be utilized when calling this method: - -```php -if (Auth::once($credentials)) { - // ... -} -``` - -## Logging Out - -To manually log users out of your application, you may use the `logout` method provided by the `Auth` facade. This will remove the authentication information from the user's session so that subsequent requests are not authenticated. - -In addition to calling the `logout` method, it is recommended that you invalidate the user's session and regenerate their [CSRF token](/docs/csrf). After logging the user out, you would typically redirect the user to the root of your application: - -```php -use Hypervel\Http\Request; -use Psr\Http\Message\ResponseInterface; -use Hypervel\Support\Facades\Auth; - -/** - * Log the user out of the application. - */ -public function logout(Request $request): ResponseInterface -{ - Auth::logout(); - - $request->session()->invalidate(); - - $request->session()->regenerateToken(); - - return redirect('/'); -} -``` - -## Password Confirmation - -While building your application, you may occasionally have actions that should require the user to confirm their password before the action is performed or before the user is redirected to a sensitive area of the application. Hypervel includes built-in middleware to make this process a breeze. Implementing this feature will require you to define two routes: one route to display a view asking the user to confirm their password and another route to confirm that the password is valid and redirect the user to their intended destination. - -### Configuration - -After confirming their password, a user will not be asked to confirm their password again for three hours. However, you may configure the length of time before the user is re-prompted for their password by changing the value of the `password_timeout` configuration value within your application's `config/auth.php` configuration file. - -### Routing - -#### The Password Confirmation Form - -First, we will define a route to display a view that requests the user to confirm their password: - -```php -Route::get('/confirm-password', function () { - return view('auth.confirm-password'); -}, ['middleware' => 'auth', 'name' => 'password.confirm']); -``` - -As you might expect, the view that is returned by this route should have a form containing a `password` field. In addition, feel free to include text within the view that explains that the user is entering a protected area of the application and must confirm their password. - -#### Confirming the Password - -Next, we will define a route that will handle the form request from the "confirm password" view. This route will be responsible for validating the password: - -```php -use Hypervel\Http\Request; -use Hypervel\Support\Facades\Hash; - -Route::post('/confirm-password', function (Request $request) { - if (! Hash::check($request->password, $request->user()->password)) { - // logics for handling unauthorized user - } - - // logics for passing confirmation -})->middleware(['auth', 'throttle:6,1']); -``` - -## Adding Custom Guards - -You may define your own authentication guards using the `extend` method on the `Auth` facade. You should place your call to the `extend` method within a [service provider](/docs/providers). Since Hypervel already ships with an `AppServiceProvider`, we can place the code in that provider: - -```php - [ - 'api' => [ - 'driver' => 'jwt', - 'provider' => 'users', - ], -], -``` - -### Closure Request Guards - -The simplest way to implement a custom, HTTP request based authentication system is by using the `Auth::viaRequest` method. This method allows you to quickly define your authentication process using a single closure. - -To get started, call the `Auth::viaRequest` method within the `boot` method of your application's `AppServiceProvider`. The `viaRequest` method accepts an authentication driver name as its first argument. This name can be any string that describes your custom guard. The second argument passed to the method should be a closure that receives the incoming HTTP request and returns a user instance or, if authentication fails, `null`: - -```php -use App\Models\User; -use Hypervel\Support\Facades\Auth; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Auth::viaRequest('custom-token', function () { - return User::where('token', (string) request()->token)->first(); - }); -} -``` - -Once your custom authentication driver has been defined, you may configure it as a driver within the `guards` configuration of your `auth.php` configuration file: - -```php -'guards' => [ - 'api' => [ - 'driver' => 'custom-token', - ], -], -``` - -Finally, you may reference the guard when assigning the authentication middleware to a route: - -```php -Route::group(function () { - // ... -}, ['middleware' => 'auth:api']); -``` - -## Adding Custom User Providers - -If you are not using a traditional relational database to store your users, you will need to extend Hypervel with your own authentication user provider. We will use the `provider` method on the `Auth` facade to define a custom user provider. The user provider resolver should return an implementation of `Hypervel\Auth\Contracts\UserProvider`: - -```php -make('mongo.connection')); - }); - } -} -``` - -After you have registered the provider using the `provider` method, you may switch to the new user provider in your `auth.php` configuration file. First, define a `provider` that uses your new driver: - -```php -'providers' => [ - 'users' => [ - 'driver' => 'mongo', - ], -], -``` - -Finally, you may reference this provider in your `guards` configuration: - -```php -'guards' => [ - 'web' => [ - 'driver' => 'session', - 'provider' => 'users', - ], -], -``` - -### The User Provider Contract - -`Hypervel\Auth\Contracts\UserProvider` implementations are responsible for fetching an `Hypervel\Auth\Contracts\Authenticatable` implementation out of a persistent storage system, such as MySQL, MongoDB, etc. These two interfaces allow the Hypervel authentication mechanisms to continue functioning regardless of how the user data is stored or what type of class is used to represent the authenticated user: - -Let's take a look at the `Hypervel\Auth\Contracts\UserProvider` contract: - -```php -getAuthPassword()` to the value of `$credentials['password']`. This method should return `true` or `false` indicating whether the password is valid. - -### The Authenticatable Contract - -Now that we have explored each of the methods on the `UserProvider`, let's take a look at the `Authenticatable` contract. Remember, user providers should return implementations of this interface from the `retrieveById` and `retrieveByCredentials` methods: - -```php -id === $post->user_id; - }); -} -``` - -Like controllers, gates may also be defined using a class callback array: - -```php -use App\Policies\PostPolicy; -use Hypervel\Support\Facades\Gate; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Gate::define('update-post', [PostPolicy::class, 'update']); -} -``` - -### Authorizing Actions - -To authorize an action using gates, you should use the `allows` or `denies` methods provided by the `Gate` facade. Note that you are not required to pass the currently authenticated user to these methods. Hypervel will automatically take care of passing the user into the gate closure. It is typical to call the gate authorization methods within your application's controllers before performing an action that requires authorization: - -```php -allows('update-post', $post)) { - // The user can update the post... -} - -if (Gate::forUser($user)->denies('update-post', $post)) { - // The user can't update the post... -} -``` - -You may authorize multiple actions at a time using the `any` or `none` methods: - -```php -if (Gate::any(['update-post', 'delete-post'], $post)) { - // The user can update or delete the post... -} - -if (Gate::none(['update-post', 'delete-post'], $post)) { - // The user can't update or delete the post... -} -``` - -#### Authorizing or Throwing Exceptions - -If you would like to attempt to authorize an action and automatically throw an `Hypervel\Auth\Access\AuthorizationException` if the user is not allowed to perform the given action, you may use the `Gate` facade's `authorize` method. Instances of `AuthorizationException` are automatically converted to a 403 HTTP response by Laravel: - -```php -Gate::authorize('update-post', $post); - -// The action is authorized... -``` - -#### Supplying Additional Context - -The gate methods for authorizing abilities (`allows`, `denies`, `check`, `any`, `none`, `authorize`, `can`, `cannot`) can receive an array as their second argument. These array elements are passed as parameters to the gate closure, and can be used for additional context when making authorization decisions: - -```php -use App\Models\Category; -use App\Models\User; -use Hypervel\Support\Facades\Gate; - -Gate::define('create-post', function (User $user, Category $category, bool $pinned) { - if (! $user->canPublishToGroup($category->group)) { - return false; - } elseif ($pinned && ! $user->canPinPosts()) { - return false; - } - - return true; -}); - -if (Gate::check('create-post', [$category, $pinned])) { - // The user can create the post... -} -``` - -### Gate Responses - -So far, we have only examined gates that return simple boolean values. However, sometimes you may wish to return a more detailed response, including an error message. To do so, you may return an `Hypervel\Auth\Access\Response` from your gate: - -```php -use App\Models\User; -use Hypervel\Auth\Access\Response; -use Hypervel\Support\Facades\Gate; - -Gate::define('edit-settings', function (User $user) { - return $user->isAdmin - ? Response::allow() - : Response::deny('You must be an administrator.'); -}); -``` - -Even when you return an authorization response from your gate, the `Gate::allows` method will still return a simple boolean value; however, you may use the `Gate::inspect` method to get the full authorization response returned by the gate: - -```php -$response = Gate::inspect('edit-settings'); - -if ($response->allowed()) { - // The action is authorized... -} else { - echo $response->message(); -} -``` - -When using the `Gate::authorize` method, which throws an `AuthorizationException` if the action is not authorized, the error message provided by the authorization response will be propagated to the HTTP response: - -```php -Gate::authorize('edit-settings'); - -// The action is authorized... -``` - -#### Customizing The HTTP Response Status - -When an action is denied via a Gate, a `403` HTTP response is returned; however, it can sometimes be useful to return an alternative HTTP status code. You may customize the HTTP status code returned for a failed authorization check using the `denyWithStatus` static constructor on the `Hypervel\Auth\Access\Response` class: - -```php -use App\Models\User; -use Hypervel\Auth\Access\Response; -use Hypervel\Support\Facades\Gate; - -Gate::define('edit-settings', function (User $user) { - return $user->isAdmin - ? Response::allow() - : Response::denyWithStatus(404); -}); -``` - -Because hiding resources via a `404` response is such a common pattern for web applications, the `denyAsNotFound` method is offered for convenience: - -```php -use App\Models\User; -use Hypervel\Auth\Access\Response; -use Hypervel\Support\Facades\Gate; - -Gate::define('edit-settings', function (User $user) { - return $user->isAdmin - ? Response::allow() - : Response::denyAsNotFound(); -}); -``` - -### Intercepting Gate Checks - -Sometimes, you may wish to grant all abilities to a specific user. You may use the `before` method to define a closure that is run before all other authorization checks: - -```php -use App\Models\User; -use Hypervel\Support\Facades\Gate; - -Gate::before(function (User $user, string $ability) { - if ($user->isAdministrator()) { - return true; - } -}); -``` - -If the `before` closure returns a non-null result that result will be considered the result of the authorization check. - -You may use the `after` method to define a closure to be executed after all other authorization checks: - -```php -use App\Models\User; - -Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) { - if ($user->isAdministrator()) { - return true; - } -}); -``` - -Values returned by `after` closures will not override the result of the authorization check unless the gate or policy returned `null`. - -### Inline Authorization - -Occasionally, you may wish to determine if the currently authenticated user is authorized to perform a given action without writing a dedicated gate that corresponds to the action. Hypervel allows you to perform these types of "inline" authorization checks via the `Gate::allowIf` and `Gate::denyIf` methods. Inline authorization does not execute any defined ["before" or "after" authorization hooks](#intercepting-gate-checks): - -```php -use App\Models\User; -use Hypervel\Support\Facades\Gate; - -Gate::allowIf(fn (User $user) => $user->isAdministrator()); - -Gate::denyIf(fn (User $user) => $user->banned()); -``` - -If the action is not authorized or if no user is currently authenticated, Hypervel will automatically throw an `Hypervel\Auth\Access\AuthorizationException` exception. Instances of `AuthorizationException` are automatically converted to a 403 HTTP response by Hypervel's exception handler. - -## Creating Policies - -### Generating Policies - -Policies are classes that organize authorization logic around a particular model or resource. For example, if your application is a blog, you may have an `App\Models\Post` model and a corresponding `App\Policies\PostPolicy` to authorize user actions such as creating or updating posts. - -You may generate a policy using the `make:policy` Artisan command. The generated policy will be placed in the `app/Policies` directory. If this directory does not exist in your application, Hypervel will create it for you: - -```shell -php artisan make:policy PostPolicy -``` - -The `make:policy` command will generate an empty policy class. If you would like to generate a class with example policy methods related to viewing, creating, updating, and deleting the resource, you may provide a `--model` option when executing the command: - -```shell -php artisan make:policy PostPolicy --model=Post -``` - -### Registering Policies - -#### Manually Registering Policies - -Using the `Gate` facade, you may manually register policies and their corresponding models within the `boot` method of your application's `AppServiceProvider`: - -```php -use App\Models\Order; -use App\Policies\OrderPolicy; -use Hypervel\Support\Facades\Gate; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Gate::policy(Order::class, OrderPolicy::class); -} -``` - -## Writing Policies - -### Policy Methods - -Once the policy class has been registered, you may add methods for each action it authorizes. For example, let's define an `update` method on our `PostPolicy` which determines if a given `App\Models\User` can update a given `App\Models\Post` instance. - -The `update` method will receive a `User` and a `Post` instance as its arguments, and should return `true` or `false` indicating whether the user is authorized to update the given `Post`. So, in this example, we will verify that the user's `id` matches the `user_id` on the post: - -```php -id === $post->user_id; - } -} -``` - -You may continue to define additional methods on the policy as needed for the various actions it authorizes. For example, you might define `view` or `delete` methods to authorize various `Post` related actions, but remember you are free to give your policy methods any name you like. - -If you used the `--model` option when generating your policy via the Artisan console, it will already contain methods for the `viewAny`, `view`, `create`, `update`, `delete`, `restore`, and `forceDelete` actions. - -::: note -All policies are resolved via the Hypervel [service container](/docs/container), allowing you to type-hint any needed dependencies in the policy's constructor to have them automatically injected. -::: - -### Policy Responses - -So far, we have only examined policy methods that return simple boolean values. However, sometimes you may wish to return a more detailed response, including an error message. To do so, you may return an `Hypervel\Auth\Access\Response` instance from your policy method: - -```php -use App\Models\Post; -use App\Models\User; -use Hypervel\Auth\Access\Response; - -/** - * Determine if the given post can be updated by the user. - */ -public function update(User $user, Post $post): Response -{ - return $user->id === $post->user_id - ? Response::allow() - : Response::deny('You do not own this post.'); -} -``` - -When returning an authorization response from your policy, the `Gate::allows` method will still return a simple boolean value; however, you may use the `Gate::inspect` method to get the full authorization response returned by the gate: - -```php -use Hypervel\Support\Facades\Gate; - -$response = Gate::inspect('update', $post); - -if ($response->allowed()) { - // The action is authorized... -} else { - echo $response->message(); -} -``` - -When using the `Gate::authorize` method, which throws an `AuthorizationException` if the action is not authorized, the error message provided by the authorization response will be propagated to the HTTP response: - -```php -Gate::authorize('update', $post); - -// The action is authorized... -``` - -#### Customizing the HTTP Response Status - -When an action is denied via a policy method, a `403` HTTP response is returned; however, it can sometimes be useful to return an alternative HTTP status code. You may customize the HTTP status code returned for a failed authorization check using the `denyWithStatus` static constructor on the `Hypervel\Auth\Access\Response` class: - -```php -use App\Models\Post; -use App\Models\User; -use Hypervel\Auth\Access\Response; - -/** - * Determine if the given post can be updated by the user. - */ -public function update(User $user, Post $post): Response -{ - return $user->id === $post->user_id - ? Response::allow() - : Response::denyWithStatus(404); -} -``` - -Because hiding resources via a `404` response is such a common pattern for web applications, the `denyAsNotFound` method is offered for convenience: - -```php -use App\Models\Post; -use App\Models\User; -use Hypervel\Auth\Access\Response; - -/** - * Determine if the given post can be updated by the user. - */ -public function update(User $user, Post $post): Response -{ - return $user->id === $post->user_id - ? Response::allow() - : Response::denyAsNotFound(); -} -``` - -### Methods Without Models - -Some policy methods only receive an instance of the currently authenticated user. This situation is most common when authorizing `create` actions. For example, if you are creating a blog, you may wish to determine if a user is authorized to create any posts at all. In these situations, your policy method should only expect to receive a user instance: - -```php -/** - * Determine if the given user can create posts. - */ -public function create(User $user): bool -{ - return $user->role == 'writer'; -} -``` - -### Guest Users - -By default, all gates and policies automatically return `false` if the incoming HTTP request was not initiated by an authenticated user. However, you may allow these authorization checks to pass through to your gates and policies by declaring an "optional" type-hint or supplying a `null` default value for the user argument definition: - -```php -id === $post->user_id; - } -} -``` - -### Policy Filters - -For certain users, you may wish to authorize all actions within a given policy. To accomplish this, define a `before` method on the policy. The `before` method will be executed before any other methods on the policy, giving you an opportunity to authorize the action before the intended policy method is actually called. This feature is most commonly used for authorizing application administrators to perform any action: - -```php -use App\Models\User; - -/** - * Perform pre-authorization checks. - */ -public function before(User $user, string $ability): bool|null -{ - if ($user->isAdministrator()) { - return true; - } - - return null; -} -``` - -If you would like to deny all authorization checks for a particular type of user then you may return `false` from the `before` method. If `null` is returned, the authorization check will fall through to the policy method. - -::: warning -The `before` method of a policy class will not be called if the class doesn't contain a method with a name matching the name of the ability being checked. -::: - -## Authorizing Actions Using Policies - -### Via the User Model - -The `App\Models\User` model that is included with your Hypervel application includes two helpful methods for authorizing actions: `can` and `cannot`. The `can` and `cannot` methods receive the name of the action you wish to authorize and the relevant model. For example, let's determine if a user is authorized to update a given `App\Models\Post` model. Typically, this will be done within a controller method: - -```php -user()->cannot('update', $post)) { - abort(403); - } - - // Update the post... - - return redirect('/posts'); - } -} -``` - -If a [policy is registered](#registering-policies) for the given model, the `can` method will automatically call the appropriate policy and return the boolean result. If no policy is registered for the model, the `can` method will attempt to call the closure-based Gate matching the given action name. - -#### Actions That Don't Require Models - -Remember, some actions may correspond to policy methods like `create` that do not require a model instance. In these situations, you may pass a class name to the `can` method. The class name will be used to determine which policy to use when authorizing the action: - -```php -user()->cannot('create', Post::class)) { - abort(403); - } - - // Create the post... - - return redirect('/posts'); - } -} -``` - -### Via the `Gate` Facade - -In addition to helpful methods provided to the `App\Models\User` model, you can always authorize actions via the `Gate` facade's `authorize` method. - -Like the `can` method, this method accepts the name of the action you wish to authorize and the relevant model. If the action is not authorized, the `authorize` method will throw an `Hypervel\Auth\Access\AuthorizationException` exception which the Hypervel exception handler will automatically convert to an HTTP response with a 403 status code: - -```php - 'can:update,post']); -``` - -In this example, we're passing the `can` middleware two arguments. The first is the name of the action we wish to authorize and the second is the route parameter we wish to pass to the policy method. In this case, since we are using [implicit model binding](/docs/routing#implicit-binding), an `App\Models\Post` model will be passed to the policy method. If the user is not authorized to perform the given action, an HTTP response with a 403 status code will be returned by the middleware. - -#### Actions That Don't Require Models - -Again, some policy methods like `create` do not require a model instance. In these situations, you may pass a class name to the middleware. The class name will be used to determine which policy to use when authorizing the action: - -```php -Route::post('/post', function () { - // The current user may create posts... -}, ['middleware' => 'can:create,App\Models\Post']); -``` - -Specifying the entire class name within a string middleware definition can become cumbersome. For that reason, you may choose to attach the `can` middleware to your route using the `can` method: - -```php -use App\Models\Post; - -Route::post('/post', function () { - // The current user may create posts... -})->can('create', Post::class); -``` - -### Via Blade Templates - -When writing Blade templates, you may wish to display a portion of the page only if the user is authorized to perform a given action. For example, you may wish to show an update form for a blog post only if the user can actually update the post. In this situation, you may use the `@can` and `@cannot` directives: - -```html -@can('update', $post) - -@elsecan('create', App\Models\Post::class) - -@else - -@endcan - -@cannot('update', $post) - -@elsecannot('create', App\Models\Post::class) - -@endcannot -``` - -These directives are convenient shortcuts for writing `@if` and `@unless` statements. The `@can` and `@cannot` statements above are equivalent to the following statements: - -```html -@if (Auth::user()->can('update', $post)) - -@endif - -@unless (Auth::user()->can('update', $post)) - -@endunless -``` - -You may also determine if a user is authorized to perform any action from a given array of actions. To accomplish this, use the `@canany` directive: - -```html -@canany(['update', 'view', 'delete'], $post) - -@elsecanany(['create'], \App\Models\Post::class) - -@endcanany -``` - -#### Actions That Don't Require Models - -Like most of the other authorization methods, you may pass a class name to the `@can` and `@cannot` directives if the action does not require a model instance: - -```html -@can('create', App\Models\Post::class) - -@endcan - -@cannot('create', App\Models\Post::class) - -@endcannot -``` - -### Supplying Additional Context - -When authorizing actions using policies, you may pass an array as the second argument to the various authorization functions and helpers. The first element in the array will be used to determine which policy should be invoked, while the rest of the array elements are passed as parameters to the policy method and can be used for additional context when making authorization decisions. For example, consider the following `PostPolicy` method definition which contains an additional `$category` parameter: - -```php -/** - * Determine if the given post can be updated by the user. - */ -public function update(User $user, Post $post, int $category): bool -{ - return $user->id === $post->user_id && - $user->canUpdateCategory($category); -} -``` - -When attempting to determine if the authenticated user can update a given post, we can invoke this policy method like so: - -```php -/** - * Update the given blog post. - * - * @throws \Hypervel\Auth\Access\AuthorizationException - */ -public function update(Request $request, Post $post): ResponseInterface -{ - Gate::authorize('update', [$post, $request->category]); - - // The current user can update the blog post... - - return redirect('/posts'); -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/blade.md b/_archive/docs/0.3/docs/blade.md deleted file mode 100644 index b0fffa56ce..0000000000 --- a/_archive/docs/0.3/docs/blade.md +++ /dev/null @@ -1,1644 +0,0 @@ -# Blade -[[toc]] - -## Introduction - -Blade is the simple, yet powerful templating engine that is included with Hypervel. Unlike some PHP templating engines, Blade does not restrict you from using plain PHP code in your templates. In fact, all Blade templates are compiled into plain PHP code and cached until they are modified, meaning Blade adds essentially zero overhead to your application. Blade template files use the `.blade.php` file extension and are typically stored in the `resources/views` directory. - -Blade views may be returned from routes or controllers using the global `view` helper. Of course, as mentioned in the documentation on [views](/docs/views), data may be passed to the Blade view using the `view` helper's second argument: - -```php -Route::get('/', function () { - return view('greeting', ['name' => 'Finn']); -}); -``` - -## Displaying Data - -You may display data that is passed to your Blade views by wrapping the variable in curly braces. For example, given the following route: - -```php -Route::get('/', function () { - return view('welcome', ['name' => 'Samantha']); -}); -``` - -You may display the contents of the `name` variable like so: - -```html -Hello, {{ $name }}. -``` - -::: note -Blade's `{{ }}` echo statements are automatically sent through PHP's `htmlspecialchars` function to prevent XSS attacks. -::: - -You are not limited to displaying the contents of the variables passed to the view. You may also echo the results of any PHP function. In fact, you can put any PHP code you wish inside of a Blade echo statement: - -```html -The current UNIX timestamp is {{ time() }}. -``` - -### HTML Entity Encoding - -By default, Blade will double encode HTML entities. If you would like to disable double encoding, call the `Blade::withoutDoubleEncoding` method from the `boot` method of your `AppServiceProvider`: - -```php -Hypervel - -Hello, @{{ name }}. -``` - -In this example, the `@` symbol will be removed by Blade; however, `{{ name }}` expression will remain untouched by the Blade engine, allowing it to be rendered by your JavaScript framework. - -The `@` symbol may also be used to escape Blade directives: - -```html -{{-- Blade template --}} -@@if() - - -@if() -``` - -#### Rendering JSON - -Sometimes you may pass an array to your view with the intention of rendering it as JSON in order to initialize a JavaScript variable. For example: - -```html - -``` - -However, instead of manually calling `json_encode`, you may use the `Hypervel\Support\Js::from` method directive. The `from` method accepts the same arguments as PHP's `json_encode` function; however, it will ensure that the resulting JSON is properly escaped for inclusion within HTML quotes. The `from` method will return a string `JSON.parse` JavaScript statement that will convert the given object or array into a valid JavaScript object: - -```html - -``` - -The latest versions of the Hypervel application skeleton include a `Js` facade, which provides convenient access to this functionality within your Blade templates: - -```html - -``` - -::: warning -You should only use the `Js::from` method to render existing variables as JSON. The Blade templating is based on regular expressions and attempts to pass a complex expression to the directive may cause unexpected failures. -::: - -#### The `@verbatim` Directive - -If you are displaying JavaScript variables in a large portion of your template, you may wrap the HTML in the `@verbatim` directive so that you do not have to prefix each Blade echo statement with an `@` symbol: - -```html -@verbatim -
- Hello, {{ name }}. -
-@endverbatim -``` - -## Blade Directives - -In addition to template inheritance and displaying data, Blade also provides convenient shortcuts for common PHP control structures, such as conditional statements and loops. These shortcuts provide a very clean, terse way of working with PHP control structures while also remaining familiar to their PHP counterparts. - -### If Statements - -You may construct `if` statements using the `@if`, `@elseif`, `@else`, and `@endif` directives. These directives function identically to their PHP counterparts: - -```html -@if (count($records) === 1) - I have one record! -@elseif (count($records) > 1) - I have multiple records! -@else - I don't have any records! -@endif -``` - -For convenience, Blade also provides an `@unless` directive: - -```html -@unless (Auth::check()) - You are not signed in. -@endunless -``` - -In addition to the conditional directives already discussed, the `@isset` and `@empty` directives may be used as convenient shortcuts for their respective PHP functions: - -```html -@isset($records) - // $records is defined and is not null... -@endisset - -@empty($records) - // $records is "empty"... -@endempty -``` - -#### Authentication Directives - -The `@auth` and `@guest` directives may be used to quickly determine if the current user is [authenticated](/docs/authentication) or is a guest: - -```html -@auth - // The user is authenticated... -@endauth - -@guest - // The user is not authenticated... -@endguest -``` - -If needed, you may specify the authentication guard that should be checked when using the `@auth` and `@guest` directives: - -```html -@auth('admin') - // The user is authenticated... -@endauth - -@guest('admin') - // The user is not authenticated... -@endguest -``` - -#### Environment Directives - -You may check if the application is running in the production environment using the `@production` directive: - -```html -@production - // Production specific content... -@endproduction -``` - -Or, you may determine if the application is running in a specific environment using the `@env` directive: - -```html -@env('staging') - // The application is running in "staging"... -@endenv - -@env(['staging', 'production']) - // The application is running in "staging" or "production"... -@endenv -``` - -#### Section Directives - -You may determine if a template inheritance section has content using the `@hasSection` directive: - -```html -@hasSection('navigation') -
- @yield('navigation') -
- -
-@endif -``` - -You may use the `sectionMissing` directive to determine if a section does not have content: - -```html -@sectionMissing('navigation') -
- @include('default-navigation') -
-@endif -``` - -#### Session Directives - -The `@session` directive may be used to determine if a [session](/docs/session) value exists. If the session value exists, the template contents within the `@session` and `@endsession` directives will be evaluated. Within the `@session` directive's contents, you may echo the `$value` variable to display the session value: - -```html -@session('status') -
- {{ $value }} -
-@endsession -``` - -### Switch Statements - -Switch statements can be constructed using the `@switch`, `@case`, `@break`, `@default` and `@endswitch` directives: - -```html -@switch($i) - @case(1) - First case... - @break - - @case(2) - Second case... - @break - - @default - Default case... -@endswitch -``` - -### Loops - -In addition to conditional statements, Blade provides simple directives for working with PHP's loop structures. Again, each of these directives functions identically to their PHP counterparts: - -```html -@for ($i = 0; $i < 10; $i++) - The current value is {{ $i }} -@endfor - -@foreach ($users as $user) -

This is user {{ $user->id }}

-@endforeach - -@forelse ($users as $user) -
  • {{ $user->name }}
  • -@empty -

    No users

    -@endforelse - -@while (true) -

    I'm looping forever.

    -@endwhile -``` - -::: important -While iterating through a `foreach` loop, you may use the [loop variable](#the-loop-variable) to gain valuable information about the loop, such as whether you are in the first or last iteration through the loop. -::: - -When using loops you may also skip the current iteration or end the loop using the `@continue` and `@break` directives: - -```html -@foreach ($users as $user) - @if ($user->type == 1) - @continue - @endif - -
  • {{ $user->name }}
  • - - @if ($user->number == 5) - @break - @endif -@endforeach -``` - -You may also include the continuation or break condition within the directive declaration: - -```html -@foreach ($users as $user) - @continue($user->type == 1) - -
  • {{ $user->name }}
  • - - @break($user->number == 5) -@endforeach -``` - -### The Loop Variable - -While iterating through a `foreach` loop, a `$loop` variable will be available inside of your loop. This variable provides access to some useful bits of information such as the current loop index and whether this is the first or last iteration through the loop: - -```html -@foreach ($users as $user) - @if ($loop->first) - This is the first iteration. - @endif - - @if ($loop->last) - This is the last iteration. - @endif - -

    This is user {{ $user->id }}

    -@endforeach -``` - -If you are in a nested loop, you may access the parent loop's `$loop` variable via the `parent` property: - -```html -@foreach ($users as $user) - @foreach ($user->posts as $post) - @if ($loop->parent->first) - This is the first iteration of the parent loop. - @endif - @endforeach -@endforeach -``` - -The `$loop` variable also contains a variety of other useful properties: - -| Property | Description | -|--------------------|--------------------------------------------------------| -| `$loop->index` | The index of the current loop iteration (starts at 0). | -| `$loop->iteration` | The current loop iteration (starts at 1). | -| `$loop->remaining` | The iterations remaining in the loop. | -| `$loop->count` | The total number of items in the array being iterated. | -| `$loop->first` | Whether this is the first iteration through the loop. | -| `$loop->last` | Whether this is the last iteration through the loop. | -| `$loop->even` | Whether this is an even iteration through the loop. | -| `$loop->odd` | Whether this is an odd iteration through the loop. | -| `$loop->depth` | The nesting level of the current loop. | -| `$loop->parent` | When in a nested loop, the parent's loop variable. | - -### Conditional Classes & Styles - -The `@class` directive conditionally compiles a CSS class string. The directive accepts an array of classes where the array key contains the class or classes you wish to add, while the value is a boolean expression. If the array element has a numeric key, it will always be included in the rendered class list: - -```html -@php - $isActive = false; - $hasError = true; -@endphp - - $isActive, - 'text-gray-500' => ! $isActive, - 'bg-red' => $hasError, -])> - - -``` - -Likewise, the `@style` directive may be used to conditionally add inline CSS styles to an HTML element: - -```html -@php - $isActive = true; -@endphp - - $isActive, -])> - - -``` - -### Including Subviews - -::: note -While you're free to use the `@include` directive, Blade [components](#components) provide similar functionality and offer several benefits over the `@include` directive such as data and attribute binding. -::: - -Blade's `@include` directive allows you to include a Blade view from within another view. All variables that are available to the parent view will be made available to the included view: - -```html -
    - @include('shared.errors') - -
    - -
    -
    -``` - -Even though the included view will inherit all data available in the parent view, you may also pass an array of additional data that should be made available to the included view: - -```html -@include('view.name', ['status' => 'complete']) -``` - -If you attempt to `@include` a view which does not exist, Hypervel will throw an error. If you would like to include a view that may or may not be present, you should use the `@includeIf` directive: - -```html -@includeIf('view.name', ['status' => 'complete']) -``` - -If you would like to `@include` a view if a given boolean expression evaluates to `true` or `false`, you may use the `@includeWhen` and `@includeUnless` directives: - -```html -@includeWhen($boolean, 'view.name', ['status' => 'complete']) - -@includeUnless($boolean, 'view.name', ['status' => 'complete']) -``` - -To include the first view that exists from a given array of views, you may use the `includeFirst` directive: - -```html -@includeFirst(['custom.admin', 'admin'], ['status' => 'complete']) -``` - -::: warning -You should avoid using the `__DIR__` and `__FILE__` constants in your Blade views, since they will refer to the location of the cached, compiled view. -::: - -### The `@once` Directive - -The `@once` directive allows you to define a portion of the template that will only be evaluated once per rendering cycle. This may be useful for pushing a given piece of JavaScript into the page's header using [stacks](#stacks). For example, if you are rendering a given [component](#components) within a loop, you may wish to only push the JavaScript to the header the first time the component is rendered: - -```html -@once - @push('scripts') - - @endpush -@endonce -``` - -Since the `@once` directive is often used in conjunction with the `@push` or `@prepend` directives, the `@pushOnce` and `@prependOnce` directives are available for your convenience: - -```html -@pushOnce('scripts') - -@endPushOnce -``` - -### Raw PHP - -In some situations, it's useful to embed PHP code into your views. You can use the Blade `@php` directive to execute a block of plain PHP within your template: - -```html -@php - $counter = 1; -@endphp -``` - -Or, if you only need to use PHP to import a class, you may use the `@use` directive: - -```html -@use('App\Models\Flight') -``` - -A second argument may be provided to the `@use` directive to alias the imported class: - -```php -@use('App\Models\Flight', 'FlightModel') -``` - -### Comments - -Blade also allows you to define comments in your views. However, unlike HTML comments, Blade comments are not included in the HTML returned by your application: - -```html -{{-- This comment will not be present in the rendered HTML --}} -``` - -## Components - -Components and slots provide similar benefits to sections, layouts, and includes; however, some may find the mental model of components and slots easier to understand. There are two approaches to writing components: class based components and anonymous components. - -To create a class based component, you may use the `make:component` Artisan command. To illustrate how to use components, we will create a simple Alert component. The `make:component` command will place the component in the `app/View/Components` directory: - -```shell:no-line-numbers -php artisan make:component Alert -``` - -```php -type = $type; - $this->message = $message; - } - public function render() - { - return View::make('components.alert'); - } -} -``` - -Then let's create a view template for the component. The view will be placed in the `resources/views/components` directory. - -```html -
    - {{ $message }} -
    -``` - -#### Manually Registering Package Components - -When writing components for your own application, components are automatically discovered within the `app/View/Components` directory and `resources/views/components` directory. - -However, if you are building a package that utilizes Blade components, you will need to manually register your component class and its HTML tag alias. You should typically register your components in the `boot` method of your package's service provider: - -```php -use Hypervel\Support\Facades\Blade; - -/** - * Bootstrap your package's services. - */ -public function boot(): void -{ - Blade::component('package-alert', Alert::class); -} -``` - -Or you can register your customized components in `config/view.php` file. - -```php -// ... -'components' => [ - 'alert' => \App\View\Component\Alert::class, -], -``` - -Once your component has been registered, it may be rendered using its tag alias: - -```html - -``` - -Alternatively, you may use the `componentNamespace` method to autoload component classes by convention. For example, a `Nightshade` package might have `Calendar` and `ColorPicker` components that reside within the `Package\Views\Components` namespace: - -```php -use Hypervel\Support\Facades\Blade; - -/** - * Bootstrap your package's services. - */ -public function boot(): void -{ - Blade::componentNamespace('Nightshade\\Views\\Components', 'nightshade'); -} -``` - -This will allow the usage of package components by their vendor namespace using the `package-name::` syntax: - -```html - - -``` - -Blade will automatically detect the class that's linked to this component by pascal-casing the component name. Subdirectories are also supported using "dot" notation. - -### Rendering Components - -To display a component, you may use a Blade component tag within one of your Blade templates. Blade component tags start with the string `x-` followed by the kebab case name of the component class: - -```html - - - -``` - -If the component class is nested deeper within the `app/View/Components` directory, you may use the `.` character to indicate directory nesting. For example, if we assume a component is located at `app/View/Components/Inputs/Button.php`, we may render it like so: - -```html - -``` - -If you would like to conditionally render your component, you may define a `shouldRender` method on your component class. If the `shouldRender` method returns `false` the component will not be rendered: - -```php -use Hypervel\Support\Str; - -/** - * Whether the component should be rendered - */ -public function shouldRender(): bool -{ - return Str::length($this->message) > 0; -} -``` - -### Passing Data to Components - -You may pass data to Blade components using HTML attributes. Hard-coded, primitive values may be passed to the component using simple HTML attribute strings. PHP expressions and variables should be passed to the component via attributes that use the `:` character as a prefix: - -```html - -``` - -You should define all of the component's data attributes in its class constructor. All public properties on a component will automatically be made available to the component's view. It is not necessary to pass the data to the view from the component's `render` method: - -```php - - {{ $message }} - -``` - -#### Casing - -Component constructor arguments should be specified using `camelCase`, while `kebab-case` should be used when referencing the argument names in your HTML attributes. For example, given the following component constructor: - -```php -/** - * Create the component instance. - */ -public function __construct( - public string $alertType, -) {} -``` - -The `$alertType` argument may be provided to the component like so: - -```html - -``` - -#### Short Attribute Syntax - -When passing attributes to components, you may also use a "short attribute" syntax. This is often convenient since attribute names frequently match the variable names they correspond to: - -```html -{{-- Short attribute syntax... --}} - - -{{-- Is equivalent to... --}} - -``` - -#### Escaping Attribute Rendering - -Since some JavaScript frameworks such as Alpine.js also use colon-prefixed attributes, you may use a double colon (`::`) prefix to inform Blade that the attribute is not a PHP expression. For example, given the following component: - -```html - - Submit - -``` - -The following HTML will be rendered by Blade: - -```html - -``` - -#### Component Methods - -In addition to public variables being available to your component template, any public methods on the component may be invoked. For example, imagine a component that has an `isSelected` method: - -```php -/** - * Determine if the given option is the currently selected option. - */ -public function isSelected(string $option): bool -{ - return $option === $this->selected; -} -``` - -You may execute this method from your component template by invoking the variable matching the name of the method: - -```html - -``` - -#### Accessing Attributes and Slots Within Component Classes - -Blade components also allow you to access the component name, attributes, and slot inside the class's render method. However, in order to access this data, you should return a closure from your component's `render` method. The closure will receive a `$data` array as its only argument. This array will contain several elements that provide information about the component: - -```php -use Closure; - -/** - * Get the view / contents that represent the component. - */ -public function render(): Closure -{ - return function (array $data) { - // $data['componentName']; - // $data['attributes']; - // $data['slot']; - - return '
    Components content
    '; - }; -} -``` - -The `componentName` is equal to the name used in the HTML tag after the `x-` prefix. So ``'s `componentName` will be `alert`. The `attributes` element will contain all of the attributes that were present on the HTML tag. The `slot` element is an `Hyperf\ViewEngine\HtmlString` instance with the contents of the component's slot. - -The closure should return a string. If the returned string corresponds to an existing view, that view will be rendered; otherwise, the returned string will be evaluated as an inline Blade view. - -#### Additional Dependencies - -If your component requires dependencies from Hypervel's [service container](/docs/container), you may list them before any of the component's data attributes and they will automatically be injected by the container: - -```php -use App\Services\AlertCreator; - -/** - * Create the component instance. - */ -public function __construct( - public AlertCreator $creator, - public string $type, - public string $message, -) {} -``` - -#### Hiding Attributes / Methods - -If you would like to prevent some public methods or properties from being exposed as variables to your component template, you may add them to an `$except` array property on your component: - -```php - -``` - -All of the attributes that are not part of the component's constructor will automatically be added to the component's "attribute bag". This attribute bag is automatically made available to the component via the `$attributes` variable. All of the attributes may be rendered within the component by echoing this variable: - -```html -
    - -
    -``` - -::: warning -Using directives such as `@env` within component tags is not supported at this time. For example, `` will not be compiled. -::: - -#### Default / Merged Attributes - -Sometimes you may need to specify default values for attributes or merge additional values into some of the component's attributes. To accomplish this, you may use the attribute bag's `merge` method. This method is particularly useful for defining a set of default CSS classes that should always be applied to a component: - -```html -
    merge(['class' => 'alert alert-'.$type]) }}> - {{ $message }} -
    -``` - -If we assume this component is utilized like so: - -```html - -``` - -The final, rendered HTML of the component will appear like the following: - -```html -
    - -
    -``` - -#### Conditionally Merge Classes - -Sometimes you may wish to merge classes if a given condition is `true`. You can accomplish this via the `class` method, which accepts an array of classes where the array key contains the class or classes you wish to add, while the value is a boolean expression. If the array element has a numeric key, it will always be included in the rendered class list: - -```html -
    class(['p-4', 'bg-red' => $hasError]) }}> - {{ $message }} -
    -``` - -If you need to merge other attributes onto your component, you can chain the `merge` method onto the `class` method: - -```html - -``` - -::: note -If you need to conditionally compile classes on other HTML elements that shouldn't receive merged attributes, you can use the [`@class` directive](#conditional-classes). -::: - -#### Non-Class Attribute Merging - -When merging attributes that are not `class` attributes, the values provided to the `merge` method will be considered the "default" values of the attribute. However, unlike the `class` attribute, these attributes will not be merged with injected attribute values. Instead, they will be overwritten. For example, a `button` component's implementation may look like the following: - -```html - -``` - -To render the button component with a custom `type`, it may be specified when consuming the component. If no type is specified, the `button` type will be used: - -```html - - Submit - -``` - -The rendered HTML of the `button` component in this example would be: - -```html - -``` - -If you would like an attribute other than `class` to have its default value and injected values joined together, you may use the `prepends` method. In this example, the `data-controller` attribute will always begin with `profile-controller` and any additional injected `data-controller` values will be placed after this default value: - -```html -
    merge(['data-controller' => $attributes->prepends('profile-controller')]) }}> - {{ $slot }} -
    -``` - -#### Retrieving and Filtering Attributes - -You may filter attributes using the `filter` method. This method accepts a closure which should return `true` if you wish to retain the attribute in the attribute bag: - -```html -{{ $attributes->filter(fn (string $value, string $key) => $key == 'foo') }} -``` - -For convenience, you may use the `whereStartsWith` method to retrieve all attributes whose keys begin with a given string: - -```html -{{ $attributes->whereStartsWith('wire:model') }} -``` - -Conversely, the `whereDoesntStartWith` method may be used to exclude all attributes whose keys begin with a given string: - -```html -{{ $attributes->whereDoesntStartWith('wire:model') }} -``` - -Using the `first` method, you may render the first attribute in a given attribute bag: - -```html -{{ $attributes->whereStartsWith('wire:model')->first() }} -``` - -If you would like to check if an attribute is present on the component, you may use the `has` method. This method accepts the attribute name as its only argument and returns a boolean indicating whether or not the attribute is present: - -```html -@if ($attributes->has('class')) -
    Class attribute is present
    -@endif -``` - -If an array is passed to the `has` method, the method will determine if all of the given attributes are present on the component: - -```html -@if ($attributes->has(['name', 'class'])) -
    All of the attributes are present
    -@endif -``` - -The `hasAny` method may be used to determine if any of the given attributes are present on the component: - -```html -@if ($attributes->hasAny(['href', ':href', 'v-bind:href'])) -
    One of the attributes is present
    -@endif -``` - -You may retrieve a specific attribute's value using the `get` method: - -```html -{{ $attributes->get('class') }} -``` - -### Reserved Keywords - -By default, some keywords are reserved for Blade's internal use in order to render components. The following keywords cannot be defined as public properties or method names within your components: - -
    - -- `data` -- `render` -- `resolveView` -- `shouldRender` -- `view` -- `withAttributes` -- `withName` - -
    - -### Slots - -You will often need to pass additional content to your component via "slots". Component slots are rendered by echoing the `$slot` variable. To explore this concept, let's imagine that an `alert` component has the following markup: - -```html - - -
    - {{ $slot }} -
    -``` - -We may pass content to the `slot` by injecting content into the component: - -```html - - Whoops! Something went wrong! - -``` - -Sometimes a component may need to render multiple different slots in different locations within the component. Let's modify our alert component to allow for the injection of a "title" slot: - -```html - - -{{ $title }} - -
    - {{ $slot }} -
    -``` - -You may define the content of the named slot using the `x-slot` tag. Any content not within an explicit `x-slot` tag will be passed to the component in the `$slot` variable: - -```xml - - - Server Error - - - Whoops! Something went wrong! - -``` - -You may invoke a slot's `isEmpty` method to determine if the slot contains content: - -```html -{{ $title }} - -
    - @if ($slot->isEmpty()) - This is default content if the slot is empty. - @else - {{ $slot }} - @endif -
    -``` - -Additionally, the `hasActualContent` method may be used to determine if the slot contains any "actual" content that is not an HTML comment: - -```html -@if ($slot->hasActualContent()) - The scope has non-comment content. -@endif -``` - -#### Scoped Slots - -If you have used a JavaScript framework such as Vue, you may be familiar with "scoped slots", which allow you to access data or methods from the component within your slot. You may achieve similar behavior in Hypervel by defining public methods or properties on your component and accessing the component within your slot via the `$component` variable. In this example, we will assume that the `x-alert` component has a public `formatAlert` method defined on its component class: - -```html - - - {{ $component->formatAlert('Server Error') }} - - - Whoops! Something went wrong! - -``` - -#### Slot Attributes - -Like Blade components, you may assign additional [attributes](#component-attributes) to slots such as CSS class names: - -```xml - - - Heading - - - Content - - - Footer - - -``` - -To interact with slot attributes, you may access the `attributes` property of the slot's variable. For more information on how to interact with attributes, please consult the documentation on [component attributes](#component-attributes): - -```html -@props([ - 'heading', - 'footer', -]) - -
    class(['border']) }}> -

    attributes->class(['text-lg']) }}> - {{ $heading }} -

    - - {{ $slot }} - -
    attributes->class(['text-gray-700']) }}> - {{ $footer }} -
    -
    -``` - -### Inline Component Views - -For very small components, it may feel cumbersome to manage both the component class and the component's view template. For this reason, you may return the component's markup directly from the `render` method: - -```php -/** - * Get the view / contents that represent the component. - */ -public function render(): string -{ - return <<<'blade' -
    - {{ $slot }} -
    - blade; -} -``` - -### Dynamic Components - -Sometimes you may need to render a component but not know which component should be rendered until runtime. In this situation, you may use Hypervel's built-in `dynamic-component` component to render the component based on a runtime value or variable: - -```html -// $componentName = "secondary-button"; - - -``` - -### Manually Registering Components - -::: important -The following documentation on manually registering components is primarily applicable to those who are writing Hypervel packages that include view components. If you are not writing a package, this portion of the component documentation may not be relevant to you. -::: - -When writing components for your own application, components are automatically discovered within the `app/View/Components` directory and `resources/views/components` directory. - -However, if you are building a package that utilizes Blade components or placing components in non-conventional directories, you will need to manually register your component class and its HTML tag alias so that Hypervel knows where to find the component. You should typically register your components in the `boot` method of your package's service provider: - -```php -use Hypervel\Support\Facades\Blade; -use VendorPackage\View\Components\AlertComponent; - -/** - * Bootstrap your package's services. - */ -public function boot(): void -{ - Blade::component('package-alert', AlertComponent::class); -} -``` - -Or you can register your components via your package's config provider: - -```php - [ - // ...others config - 'components' => [ - 'package-alert' => \App\View\Component\Alert::class, - ], - ], - ]; - } -} -``` - -Once your component has been registered, it may be rendered using its tag alias: - -```html - -``` - -#### Autoloading Package Components - -Alternatively, you may use the `componentNamespace` method to autoload component classes by convention. For example, a `Nightshade` package might have `Calendar` and `ColorPicker` components that reside within the `Package\Views\Components` namespace: - -```php -use Hypervel\Support\Facades\Blade; - -/** - * Bootstrap your package's services. - */ -public function boot(): void -{ - Blade::componentNamespace('Nightshade\\Views\\Components', 'nightshade'); -} -``` - -Or you can register your components namespace via your package's config provider: - -```php - [ - // ...others config - 'namespaces' => [ - 'package-name' => __DIR__ . '/../views', - ], - ], - ]; - } -} -``` - -This will allow the usage of package components by their vendor namespace using the `package-name::` syntax: - -```html - - -``` - -Blade will automatically detect the class that's linked to this component by pascal-casing the component name. Subdirectories are also supported using "dot" notation. - -## Anonymous Components - -Similar to inline components, anonymous components provide a mechanism for managing a component via a single file. However, anonymous components utilize a single view file and have no associated class. To define an anonymous component, you only need to place a Blade template within your `resources/views/components` directory. For example, assuming you have defined a component at `resources/views/components/alert.blade.php`, you may simply render it like so: - -```html - -``` - -You may use the `.` character to indicate if a component is nested deeper inside the `components` directory. For example, assuming the component is defined at `resources/views/components/inputs/button.blade.php`, you may render it like so: - -```html - -``` - -### Anonymous Index Components - -Sometimes, when a component is made up of many Blade templates, you may wish to group the given component's templates within a single directory. For example, imagine an "accordion" component with the following directory structure: - -```none -/resources/views/components/accordion.blade.php -/resources/views/components/accordion/item.blade.php -``` - -This directory structure allows you to render the accordion component and its item like so: - -```html - - - ... - - -``` - -However, in order to render the accordion component via `x-accordion`, we were forced to place the "index" accordion component template in the `resources/views/components` directory instead of nesting it within the `accordion` directory with the other accordion related templates. - -Thankfully, Blade allows you to place an `index.blade.php` file within a component's template directory. When an `index.blade.php` template exists for the component, it will be rendered as the "root" node of the component. So, we can continue to use the same Blade syntax given in the example above; however, we will adjust our directory structure like so: - -```none -/resources/views/components/accordion/index.blade.php -/resources/views/components/accordion/item.blade.php -``` - -### Data Properties / Attributes - -Since anonymous components do not have any associated class, you may wonder how you may differentiate which data should be passed to the component as variables and which attributes should be placed in the component's [attribute bag](#component-attributes). - -You may specify which attributes should be considered data variables using the `@props` directive at the top of your component's Blade template. All other attributes on the component will be available via the component's attribute bag. If you wish to give a data variable a default value, you may specify the variable's name as the array key and the default value as the array value: - -```html - - -@props(['type' => 'info', 'message']) - -
    merge(['class' => 'alert alert-'.$type]) }}> - {{ $message }} -
    -``` - -Given the component definition above, we may render the component like so: - -```html - -``` - -## Building Layouts - -### Layouts Using Components - -Most web applications maintain the same general layout across various pages. It would be incredibly cumbersome and hard to maintain our application if we had to repeat the entire layout HTML in every view we create. Thankfully, it's convenient to define this layout as a single [Blade component](#components) and then use it throughout our application. - -#### Defining the Layout Component - -For example, imagine we are building a "todo" list application. We might define a `layout` component that looks like the following: - -```html - - - - - {{ $title ?? 'Todo Manager' }} - - -

    Todos

    -
    - {{ $slot }} - - -``` - -#### Applying the Layout Component - -Once the `layout` component has been defined, we may create a Blade view that utilizes the component. In this example, we will define a simple view that displays our task list: - -```html - - - - @foreach ($tasks as $task) - {{ $task }} - @endforeach - -``` - -Remember, content that is injected into a component will be supplied to the default `$slot` variable within our `layout` component. As you may have noticed, our `layout` also respects a `$title` slot if one is provided; otherwise, a default title is shown. We may inject a custom title from our task list view using the standard slot syntax discussed in the [component documentation](#components): - -```html - - - - - Custom Title - - - @foreach ($tasks as $task) - {{ $task }} - @endforeach - -``` - -Now that we have defined our layout and task list views, we just need to return the `task` view from a route: - -```php -use App\Models\Task; - -Route::get('/tasks', function () { - return view('tasks', ['tasks' => Task::all()]); -}); -``` - -### Layouts Using Template Inheritance - -#### Defining a Layout - -Layouts may also be created via "template inheritance". This was the primary way of building applications prior to the introduction of [components](#components). - -To get started, let's take a look at a simple example. First, we will examine a page layout. Since most web applications maintain the same general layout across various pages, it's convenient to define this layout as a single Blade view: - -```html - - - - - App Name - @yield('title') - - - @section('sidebar') - This is the master sidebar. - @show - -
    - @yield('content') -
    - - -``` - -As you can see, this file contains typical HTML mark-up. However, take note of the `@section` and `@yield` directives. The `@section` directive, as the name implies, defines a section of content, while the `@yield` directive is used to display the contents of a given section. - -Now that we have defined a layout for our application, let's define a child page that inherits the layout. - -#### Extending a Layout - -When defining a child view, use the `@extends` Blade directive to specify which layout the child view should "inherit". Views which extend a Blade layout may inject content into the layout's sections using `@section` directives. Remember, as seen in the example above, the contents of these sections will be displayed in the layout using `@yield`: - -```html - - -@extends('layouts.app') - -@section('title', 'Page Title') - -@section('sidebar') - @@parent - -

    This is appended to the master sidebar.

    -@endsection - -@section('content') -

    This is my body content.

    -@endsection -``` - -In this example, the `sidebar` section is utilizing the `@@parent` directive to append (rather than overwriting) content to the layout's sidebar. The `@@parent` directive will be replaced by the content of the layout when the view is rendered. - -::: note -Contrary to the previous example, this `sidebar` section ends with `@endsection` instead of `@show`. The `@endsection` directive will only define a section while `@show` will define and **immediately yield** the section. -::: - -The `@yield` directive also accepts a default value as its second parameter. This value will be rendered if the section being yielded is undefined: - -```html -@yield('content', 'Default content') -``` - -### Validation Errors - -The `@error` directive may be used to quickly check if [validation error messages](/docs/validation#quick-displaying-the-validation-errors) exist for a given attribute. Within an `@error` directive, you may echo the `$message` variable to display the error message: - -```html - - - - - - -@error('title') -
    {{ $message }}
    -@enderror -``` - -Since the `@error` directive compiles to an "if" statement, you may use the `@else` directive to render content when there is not an error for an attribute: - -```html - - - - - -``` - -You may pass [the name of a specific error bag](/docs/validation#named-error-bags) as the second parameter to the `@error` directive to retrieve validation error messages on pages containing multiple forms: - -```html - - - - - - -@error('email', 'login') -
    {{ $message }}
    -@enderror -``` - -## Stacks - -Blade allows you to push to named stacks which can be rendered somewhere else in another view or layout. This can be particularly useful for specifying any JavaScript libraries required by your child views: - -```html -@push('scripts') - -@endpush -``` - -You may push to a stack as many times as needed. To render the complete stack contents, pass the name of the stack to the `@stack` directive: - -```html - - - - @stack('scripts') - -``` - -If you would like to prepend content onto the beginning of a stack, you should use the `@prepend` directive: - -```html -@push('scripts') - This will be second... -@endpush - -// Later... - -@prepend('scripts') - This will be first... -@endprepend -``` - -## Service Injection - -The `@inject` directive may be used to retrieve a service from the Hypervel [service container](/docs/container). The first argument passed to `@inject` is the name of the variable the service will be placed into, while the second argument is the class or interface name of the service you wish to resolve: - -```html -@inject('metrics', 'App\Services\MetricsService') - -
    - Monthly Revenue: {{ $metrics->monthlyRevenue() }}. -
    -``` - -## Extending Blade - -Blade allows you to define your own custom directives using the `directive` method. When the Blade compiler encounters the custom directive, it will call the provided callback with the expression that the directive contains. - -The following example creates a `@datetime($var)` directive which formats a given `$var`, which should be an instance of `DateTime`: - -```php -format('m/d/Y H:i'); ?>"; - }); - } -} -``` - -As you can see, we will chain the `format` method onto whatever expression is passed into the directive. So, in this example, the final PHP generated by this directive will be: - -```php -format('m/d/Y H:i'); ?> -``` - -::: important -After updating the logic of a Blade directive, you will need to delete all of the cached Blade views. -::: - -### Custom Echo Handlers - -If you attempt to "echo" an object using Blade, the object's `__toString` method will be invoked. The [`__toString`](https://www.php.net/manual/en/language.oop5.magic.php#object.tostring) method is one of PHP's built-in "magic methods". However, sometimes you may not have control over the `__toString` method of a given class, such as when the class that you are interacting with belongs to a third-party library. - -In these cases, Blade allows you to register a custom echo handler for that particular type of object. To accomplish this, you should invoke Blade's `stringable` method. The `stringable` method accepts a closure. This closure should type-hint the type of object that it is responsible for rendering. Typically, the `stringable` method should be invoked within the `boot` method of your application's `AppServiceProvider` class: - -```php -use Hypervel\Support\Facades\Blade; -use Money\Money; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Blade::stringable(function (Money $money) { - return $money->formatTo('en_GB'); - }); -} -``` - -Once your custom echo handler has been defined, you may simply echo the object in your Blade template: - -```html -Cost: {{ $money }} -``` - -### Custom If Statements - -Programming a custom directive is sometimes more complex than necessary when defining simple, custom conditional statements. For that reason, Blade provides a `Blade::if` method which allows you to quickly define custom conditional directives using closures. For example, let's define a custom conditional that checks the configured default "disk" for the application. We may do this in the `boot` method of our `AppServiceProvider`: - -```php -use Hypervel\Support\Facades\Blade; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Blade::if('disk', function (string $value) { - return config('filesystems.default') === $value; - }); -} -``` - -Once the custom conditional has been defined, you can use it within your templates: - -```html -@disk('local') - -@elsedisk('s3') - -@else - -@enddisk - -@unlessdisk('local') - -@enddisk -``` diff --git a/_archive/docs/0.3/docs/broadcasting.md b/_archive/docs/0.3/docs/broadcasting.md deleted file mode 100644 index fdf4f50aa2..0000000000 --- a/_archive/docs/0.3/docs/broadcasting.md +++ /dev/null @@ -1,1131 +0,0 @@ -# Broadcasting -[[toc]] - -## Introduction - -In many modern web applications, WebSockets are used to implement realtime, live-updating user interfaces. When some data is updated on the server, a message is typically sent over a WebSocket connection to be handled by the client. WebSockets provide a more efficient alternative to continually polling your application's server for data changes that should be reflected in your UI. - -For example, imagine your application is able to export a user's data to a CSV file and email it to them. However, creating this CSV file takes several minutes so you choose to create and mail the CSV within a [queued job](/docs/queues). When the CSV has been created and mailed to the user, we can use event broadcasting to dispatch an `App\Events\UserDataExported` event that is received by our application's JavaScript. Once the event is received, we can display a message to the user that their CSV has been emailed to them without them ever needing to refresh the page. - -To assist you in building these types of features, Hypervel makes it easy to "broadcast" your server-side Hypervel [events](/docs/events) over a WebSocket connection. Broadcasting your Hypervel events allows you to share the same event names and data between your server-side Hypervel application and your client-side JavaScript application. - -The core concepts behind broadcasting are simple: clients connect to named channels on the frontend, while your Hypervel application broadcasts events to these channels on the backend. These events can contain any additional data you wish to make available to the frontend. - -## Server Side Installation - -To get started using Hypervel's event broadcasting, we need to do some configuration within the Hypervel application as well as install a few packages. - -Event broadcasting is accomplished by a server-side broadcasting driver that broadcasts your Hypervel events so that Laravel Echo (a JavaScript library) can receive them within the browser client. Don't worry - we'll walk through each part of the installation process step-by-step. - -### Configuration - -All of your application's event broadcasting configuration is stored in the `config/broadcasting.php` configuration file. Hypervel supports several broadcast drivers out of the box: [Pusher Channels](https://pusher.com/channels), [Redis](/docs/redis), and a `log` driver for local development and debugging. Additionally, a `null` driver is included which allows you to totally disable broadcasting during testing. A configuration example is included for each of these drivers in the `config/broadcasting.php` configuration file. - -### Broadcast Service Provider - -Before broadcasting any events, you will first need to register the `App\Providers\BroadcastServiceProvider`. In new Hypervel applications, you only need to uncomment this provider in the `providers` array of your `config/app.php` configuration file. This `BroadcastServiceProvider` contains the code necessary to register the broadcast authorization routes and callbacks. - -### Queue Configuration - -You will also need to configure and run a [queue worker](/docs/queues). All event broadcasting is done via queued jobs so that the response time of your application is not seriously affected by events being broadcast. - -### Reverb - -While Hypervel doesn't have built-in Reverb support, you have the option to set up Reverb separately by installing it in an independent Laravel project and running it from there. You may install Reverb using the Composer package manager in your Laravel project: - -```shell:no-line-numbers -composer require laravel/reverb -``` - -Once the package is installed, you may run Reverb's installation command to publish the configuration, update your applications's broadcasting configuration, and add Reverb's required environment variables: - -```shell:no-line-numbers -php artisan reverb:install -``` - -You can find detailed Reverb installation and usage instructions in the [Reverb documentation](https://laravel.com/docs/master/reverb). - -### Pusher Channels - -If you plan to broadcast your events using [Pusher Channels](https://pusher.com/channels), you should install the Pusher Channels PHP SDK using the Composer package manager: - -```shell:no-line-numbers -composer require pusher/pusher-php-server -``` - -Next, you should configure your Pusher Channels credentials in the `config/broadcasting.php` configuration file. An example Pusher Channels configuration is already included in this file, allowing you to quickly specify your key, secret, and application ID. Typically, these values should be set via the `PUSHER_APP_KEY`, `PUSHER_APP_SECRET`, and `PUSHER_APP_ID` [environment variables](/docs/configuration#environment-configuration): - -``` -PUSHER_APP_ID=your-pusher-app-id -PUSHER_APP_KEY=your-pusher-key -PUSHER_APP_SECRET=your-pusher-secret -PUSHER_APP_CLUSTER=mt1 -``` - -The `config/broadcasting.php` file's `pusher` configuration also allows you to specify additional `options` that are supported by Channels, such as the cluster. - -Next, you will need to change your broadcast driver to `pusher` in your `.env` file: - -``` -BROADCAST_DRIVER=pusher -``` - -Finally, you are ready to install and configure [Laravel Echo](/docs/broadcasting#client-side-installation), which will receive the broadcast events on the client-side. - -### Open Source Pusher Alternatives - -[soketi](https://docs.soketi.app) provides a Pusher compatible WebSocket server for Laravel, allowing you to leverage the full power of Laravel broadcasting without a commercial WebSocket provider. For more information on installing and using open source packages for broadcasting, please consult our documentation on [open source alternatives](/docs/broadcasting#open-source-alternatives). - -### Ably - -::: note -The documentation below discusses how to use Ably in "Pusher compatibility" mode. However, the Ably team recommends and maintains a broadcaster and Echo client that is able to take advantage of the unique capabilities offered by Ably. For more information on using the Ably maintained drivers, please [consult Ably's Laravel broadcaster documentation](https://github.com/ably/laravel-broadcaster). -::: - -If you plan to broadcast your events using [Ably](https://ably.com), you should install the Ably PHP SDK using the Composer package manager: - -```shell:no-line-numbers -composer require ably/ably-php -``` - -Next, you should configure your Ably credentials in the `config/broadcasting.php` configuration file. An example Ably configuration is already included in this file, allowing you to quickly specify your key. Typically, this value should be set via the `ABLY_KEY` [environment variable](/docs/configuration#environment-configuration): - -``` -ABLY_KEY=your-ably-key -``` - -Next, you will need to change your broadcast driver to `ably` in your `.env` file: - -``` -BROADCAST_DRIVER=ably -``` - -Finally, you are ready to install and configure [Laravel Echo](/docs/broadcasting#client-side-installation), which will receive the broadcast events on the client-side. - -### Open Source Alternatives - -#### Node - -[Soketi](https://github.com/soketi/soketi) is a Node based, Pusher compatible WebSocket server for Laravel. Under the hood, Soketi utilizes µWebSockets.js for extreme scalability and speed. This package allows you to leverage the full power of Laravel broadcasting without a commercial WebSocket provider. For more information on installing and using this package, please consult its [official documentation](https://docs.soketi.app). - -## Client Side Installation - -### Reverb - -[Laravel Echo](https://github.com/laravel/echo) is a JavaScript library that makes it painless to subscribe to channels and listen for events broadcast by your server-side broadcasting driver. You may install Echo via the NPM package manager. In this example, we will also install the `pusher-js` package since Reverb utilizes the Pusher protocol for WebSocket subscriptions, channels, and messages: - -```shell:no-line-numbers -npm install --save-dev laravel-echo pusher-js -``` - -Once Echo is installed, you are ready to create a fresh Echo instance in your application's JavaScript. A great place to do this is at the bottom of the `resources/js/bootstrap.js` file that is included with the Laravel framework. By default, an example Echo configuration is already included in this file - you simply need to uncomment it and update the `broadcaster` configuration option to `reverb`: - -```js -import Echo from 'laravel-echo'; - -import Pusher from 'pusher-js'; -window.Pusher = Pusher; - -window.Echo = new Echo({ - broadcaster: 'reverb', - key: import.meta.env.VITE_REVERB_APP_KEY, - wsHost: import.meta.env.VITE_REVERB_HOST, - wsPort: import.meta.env.VITE_REVERB_PORT, - wssPort: import.meta.env.VITE_REVERB_PORT, - forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', - enabledTransports: ['ws', 'wss'], -}); -``` - -Next, you should compile your application's assets: - -```shell:no-line-numbers -npm run build -``` - -::: important -The Laravel Echo `reverb` broadcaster requires laravel-echo v1.16.0+. -::: - -### Pusher Channels - -[Laravel Echo](https://github.com/laravel/echo) is a JavaScript library that makes it painless to subscribe to channels and listen for events broadcast by your server-side broadcasting driver. You may install Echo via the NPM package manager. In this example, we will also install the `pusher-js` package since we will be using the Pusher Channels broadcaster: - -```shell:no-line-numbers -npm install --save-dev laravel-echo pusher-js -``` - -Once Echo is installed, you are ready to create a fresh Echo instance in your application's JavaScript. A great place to do this is at the bottom of the `resources/js/bootstrap.js` file that is included with the Hypervel framework. By default, an example Echo configuration is already included in this file - you simply need to uncomment it: - -```js -import Echo from 'laravel-echo'; -import Pusher from 'pusher-js'; - -window.Pusher = Pusher; - -window.Echo = new Echo({ - broadcaster: 'pusher', - key: import.meta.env.VITE_PUSHER_APP_KEY, - cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER, - forceTLS: true -}); -``` - -Once you have uncommented and adjusted the Echo configuration according to your needs, you may compile your application's assets: - -```shell:no-line-numbers -npm run build -``` - -#### Using an Existing Client Instance - -If you already have a pre-configured Pusher Channels client instance that you would like Echo to utilize, you may pass it to Echo via the `client` configuration option: - -```js -import Echo from 'laravel-echo'; -import Pusher from 'pusher-js'; - -const options = { - broadcaster: 'pusher', - key: 'your-pusher-channels-key' -} - -window.Echo = new Echo({ - ...options, - client: new Pusher(options.key, options) -}); -``` - -### Ably - -::: note -The documentation below discusses how to use Ably in "Pusher compatibility" mode. However, the Ably team recommends and maintains a broadcaster and Echo client that is able to take advantage of the unique capabilities offered by Ably. For more information on using the Ably maintained drivers, please [consult Ably's Laravel broadcaster documentation](https://github.com/ably/laravel-broadcaster). -::: - -[Laravel Echo](https://github.com/laravel/echo) is a JavaScript library that makes it painless to subscribe to channels and listen for events broadcast by your server-side broadcasting driver. You may install Echo via the NPM package manager. In this example, we will also install the `pusher-js` package. - -You may wonder why we would install the `pusher-js` JavaScript library even though we are using Ably to broadcast our events. Thankfully, Ably includes a Pusher compatibility mode which lets us use the Pusher protocol when listening for events in our client-side application: - -```shell:no-line-numbers -npm install --save-dev laravel-echo pusher-js -``` - -> Before continuing, you should enable Pusher protocol support in your Ably application settings. You may enable this feature within the "Protocol Adapter Settings" portion of your Ably application's settings dashboard. - -Once Echo is installed, you are ready to create a fresh Echo instance in your application's JavaScript. A great place to do this is at the bottom of the `resources/js/bootstrap.js` file that is included with the Laravel framework. By default, an example Echo configuration is already included in this file; however, the default configuration in the `bootstrap.js` file is intended for Pusher. You may copy the configuration below to transition your configuration to Ably: - -```js -import Echo from 'laravel-echo'; -import Pusher from 'pusher-js'; - -window.Pusher = Pusher; - -window.Echo = new Echo({ - broadcaster: 'pusher', - key: import.meta.env.VITE_ABLY_PUBLIC_KEY, - wsHost: 'realtime-pusher.ably.io', - wsPort: 443, - disableStats: true, - encrypted: true, -}); -``` - -Note that our Ably Echo configuration references a `VITE_ABLY_PUBLIC_KEY` environment variable. This variable's value should be your Ably public key. Your public key is the portion of your Ably key that occurs before the : character. - -Once you have uncommented and adjusted the Echo configuration according to your needs, you may compile your application's assets: - -```shell:no-line-numbers -npm run dev -``` - -## Concept Overview - -Hypervel's event broadcasting allows you to broadcast your server-side Laravel events to your client-side JavaScript application using a driver-based approach to WebSockets. Currently, Hypervel ships with [Pusher Channels](https://pusher.com/channels) and [Ably](https://ably.com) drivers. The events may be easily consumed on the client-side using the [Laravel Echo](/docs/broadcasting#client-side-installation) JavaScript package. - -Events are broadcast over "channels", which may be specified as public or private. Any visitor to your application may subscribe to a public channel without any authentication or authorization; however, in order to subscribe to a private channel, a user must be authenticated and authorized to listen on that channel. - -::: note -If you would like to explore open source alternatives to Pusher, check out the [open source alternatives](/docs/broadcasting#open-source-alternatives). -::: - -### Using an Example Application - -Before diving into each component of event broadcasting, let's take a high level overview using an e-commerce store as an example. - -In our application, let's assume we have a page that allows users to view the shipping status for their orders. Let's also assume that an `OrderShipmentStatusUpdated` event is fired when a shipping status update is processed by the application: - -```php -use App\Events\OrderShipmentStatusUpdated; - -OrderShipmentStatusUpdated::dispatch($order); -``` - -#### The `ShouldBroadcast` Interface - -When a user is viewing one of their orders, we don't want them to have to refresh the page to view status updates. Instead, we want to broadcast the updates to the application as they are created. So, we need to mark the `OrderShipmentStatusUpdated` event with the `ShouldBroadcast` interface. This will instruct Laravel to broadcast the event when it is fired: - -```php -order->id); -} -``` - -If you wish the event to broadcast on multiple channels, you may return an `array` instead: - -```php -use Hypervel\Broadcasting\PrivateChannel; - -/** - * Get the channels the event should broadcast on. - * - * @return array - */ -public function broadcastOn(): array -{ - return [ - new PrivateChannel('orders.'.$this->order->id), - // ... - ]; -} -``` - -#### Authorizing Channels - -Remember, users must be authorized to listen on private channels. We may define our channel authorization rules in our application's `routes/channels.php` file. In this example, we need to verify that any user attempting to listen on the private `orders.1` channel is actually the creator of the order: - -```php -use App\Models\Order; -use App\Models\User; - -Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) { - return $user->id === Order::findOrNew($orderId)->user_id; -}); -``` - -The `channel` method accepts two arguments: the name of the channel and a callback which returns `true` or `false` indicating whether the user is authorized to listen on the channel. - -All authorization callbacks receive the currently authenticated user as their first argument and any additional wildcard parameters as their subsequent arguments. In this example, we are using the `{orderId}` placeholder to indicate that the "ID" portion of the channel name is a wildcard. - -#### Listening for Event Broadcasts - -Next, all that remains is to listen for the event in our JavaScript application. We can do this using [Laravel Echo](/docs/broadcasting#client-side-installation). First, we'll use the `private` method to subscribe to the private channel. Then, we may use the `listen` method to listen for the `OrderShipmentStatusUpdated` event. By default, all of the event's public properties will be included on the broadcast event: - -```js -Echo.private(`orders.${orderId}`) - .listen('OrderShipmentStatusUpdated', (e) => { - console.log(e.order); - }); -``` - -## Defining Broadcast Events - -To inform Hypervel that a given event should be broadcast, you must implement the `Hypervel\Broadcasting\Contracts\ShouldBroadcast` interface on the event class. This interface is already imported into all event classes generated by the framework so you may easily add it to any of your events. - -The `ShouldBroadcast` interface requires you to implement a single method: `broadcastOn`. The `broadcastOn` method should return a channel or array of channels that the event should broadcast on. The channels should be instances of `Channel`, `PrivateChannel`, or `PresenceChannel`. Instances of `Channel` represent public channels that any user may subscribe to, while `PrivateChannels` and `PresenceChannels` represent private channels that require [channel authorization](/docs/broadcasting#authorizing-channels): - -```php - - */ - public function broadcastOn(): array - { - return [ - new PrivateChannel('user.'.$this->user->id), - ]; - } -} -``` - -After implementing the `ShouldBroadcast` interface, you only need to fire the [event](/docs/events) as you normally would. Once the event has been fired, a [queued job](/docs/queues) will automatically broadcast the event using your specified broadcast driver. - -### Broadcast Name - -By default, Hypervel will broadcast the event using the event's class name. However, you may customize the broadcast name by defining a `broadcastAs` method on the event: - -```php -/** - * The event's broadcast name. - */ -public function broadcastAs(): string -{ - return 'server.created'; -} -``` - -If you customize the broadcast name using the `broadcastAs` method, you should make sure to register your listener with a leading `.` character. This will instruct Echo to not prepend the application's namespace to the event: - -```js -.listen('.server.created', function (e) { - .... -}); -``` - -### Broadcast Data - -When an event is broadcast, all of its `public` properties are automatically serialized and broadcast as the event's payload, allowing you to access any of its public data from your JavaScript application. So, for example, if your event has a single public `$user` property that contains an Eloquent model, the event's broadcast payload would be: - -```json -{ - "user": { - "id": 1, - "name": "Patrick Stewart" - ... - } -} -``` - -However, if you wish to have more fine-grained control over your broadcast payload, you may add a `broadcastWith` method to your event. This method should return the array of data that you wish to broadcast as the event payload: - -```php -/** - * Get the data to broadcast. - * - * @return array - */ -public function broadcastWith(): array -{ - return ['id' => $this->user->id]; -} -``` - -### Broadcast Queue - -By default, each broadcast event is placed on the default queue for the default queue connection specified in your `queue.php` configuration file. You may customize the queue connection and name used by the broadcaster by defining `connection` and `queue` properties on your event class: - -```php -/** - * The name of the queue connection to use when broadcasting the event. - */ -public string $connection = 'redis'; - -/** - * The name of the queue on which to place the broadcasting job. - */ -public string $queue = 'default'; -``` - -Alternatively, you may customize the queue name by defining a `broadcastQueue` method on your event: - -```php -/** - * The name of the queue on which to place the broadcasting job. - */ -public function broadcastQueue(): string -{ - return 'default'; -} -``` - -If you would like to broadcast your event using the `sync` queue instead of the default queue driver, you can implement the `ShouldBroadcastNow` interface instead of `ShouldBroadcast`: - -```php -order->value > 100; -} -``` - -#### Broadcasting and Database Transactions - -When broadcast events are dispatched within database transactions, they may be processed by the queue before the database transaction has committed. When this happens, any updates you have made to models or database records during the database transaction may not yet be reflected in the database. In addition, any models or database records created within the transaction may not exist in the database. If your event depends on these models, unexpected errors can occur when the job that broadcasts the event is processed. - -If your queue connection's `after_commit` configuration option is set to `false`, you may still indicate that a particular broadcast event should be dispatched after all open database transactions have been committed by implementing the `ShouldDispatchAfterCommit` interface on the event class: - -```php - { - return { - authorize: (socketId, callback) => { - axios.post('/api/broadcasting/auth', { - socket_id: socketId, - channel_name: channel.name - }) - .then(response => { - callback(null, response.data); - }) - .catch(error => { - callback(error); - }); - } - }; - }, -}) -``` - -### Defining Authorization Callbacks - -Next, we need to define the logic that will actually determine if the currently authenticated user can listen to a given channel. This is done in the `routes/channels.php` file that is included with your application. In this file, you may use the `Broadcast::channel` method to register channel authorization callbacks: - -```php -use App\Models\User; - -Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) { - return $user->id === Order::findOrNew($orderId)->user_id; -}); -``` - -The `channel` method accepts two arguments: the name of the channel and a callback which returns `true` or `false` indicating whether the user is authorized to listen on the channel. - -All authorization callbacks receive the currently authenticated user as their first argument and any additional wildcard parameters as their subsequent arguments. In this example, we are using the `{orderId}` placeholder to indicate that the "ID" portion of the channel name is a wildcard. - -> `channel:list` is not supported yet. - -#### Authorization Callback Model Binding - -Just like HTTP routes, channel routes may also take advantage of implicit and explicit [route model binding](/docs/routing#route-model-binding). For example, instead of receiving a string or numeric order ID, you may request an actual Order model instance: - -```php -use App\Models\Order; -use App\Models\User; - -Broadcast::channel('orders.{order}', function (User $user, Order $order) { - return $user->id === $order->user_id; -}); -``` - -::: warning -Channel model binding does not support automatic implicit model binding scoping. However, this is rarely a problem because most channels can be scoped based on a single model's unique, primary key. -::: - -#### Authorization Callback Authentication - -Private and presence broadcast channels authenticate the current user via your application's default authentication guard. If the user is not authenticated, channel authorization is automatically denied and the authorization callback is never executed. However, you may assign multiple, custom guards that should authenticate the incoming request if necessary: - -```php -Broadcast::channel('channel', function () { - // ... -}, ['guards' => ['web', 'admin']]); -``` - -### Defining Channel Classes - -If your application is consuming many different channels, your `routes/channels.php` file could become bulky. So, instead of using closures to authorize channels, you may use channel classes. To generate a channel class, use the `make:channel` Artisan command. This command will place a new channel class in the `App/Broadcasting` directory. - -```shell:no-line-numbers -php artisan make:channel OrderChannel -``` - -Next, register your channel in your `routes/channels.php` file: - -```php -use App\Broadcasting\OrderChannel; - -Broadcast::channel('orders.{order}', OrderChannel::class); -``` - -Finally, you may place the authorization logic for your channel in the channel class' `join` method. This `join` method will house the same logic you would have typically placed in your channel authorization closure. You may also take advantage of channel model binding: - -```php -id === $order->user_id; - } -} -``` - -::: tip -Like many other classes in Hypervel, channel classes will automatically be resolved by the service container. So, you may type-hint any dependencies required by your channel in its constructor. -::: - -## Broadcasting Events - -Once you have defined an event and marked it with the `ShouldBroadcast` interface, you only need to fire the event using the event's dispatch method. The event dispatcher will notice that the event is marked with the `ShouldBroadcast` interface and will queue the event for broadcasting: - -```php -use App\Events\OrderShipmentStatusUpdated; - -OrderShipmentStatusUpdated::dispatch($order); -``` - -### Only to Others - -When building an application that utilizes event broadcasting, you may occasionally need to broadcast an event to all subscribers to a given channel except for the current user. You may accomplish this using the `broadcast` helper and the `toOthers` method: - -```php -use App\Events\OrderShipmentStatusUpdated; - -broadcast(new OrderShipmentStatusUpdated($update))->toOthers(); -``` - -To better understand when you may want to use the `toOthers` method, let's imagine a task list application where a user may create a new task by entering a task name. To create a task, your application might make a request to a `/task` URL which broadcasts the task's creation and returns a JSON representation of the new task. When your JavaScript application receives the response from the end-point, it might directly insert the new task into its task list like so: - -```js -axios.post('/task', task) - .then((response) => { - this.tasks.push(response.data); - }); -``` - -However, remember that we also broadcast the task's creation. If your JavaScript application is also listening for this event in order to add tasks to the task list, you will have duplicate tasks in your list: one from the end-point and one from the broadcast. You may solve this by using the `toOthers` method to instruct the broadcaster to not broadcast the event to the current user. - -::: important -Your event must use the `Hypervel\Broadcasting\InteractsWithSockets` trait in order to call the `toOthers` method. -::: - -#### Configuration - -When you initialize a Laravel Echo instance, a socket ID is assigned to the connection. If you are using a global [Axios](https://github.com/mzabriskie/axios) instance to make HTTP requests from your JavaScript application, the socket ID will automatically be attached to every outgoing request as an `X-Socket-ID` header. Then, when you call the `toOthers` method, Hypervel will extract the socket ID from the header and instruct the broadcaster to not broadcast to any connections with that socket ID. - -If you are not using a global Axios instance, you will need to manually configure your JavaScript application to send the `X-Socket-ID` header with all outgoing requests. You may retrieve the socket ID using the `Echo.socketId` method: - -```js -var socketId = Echo.socketId(); -``` - -### Customizing the Connection - -If your application interacts with multiple broadcast connections and you want to broadcast an event using a broadcaster other than your default, you may specify which connection to push an event to using the `via` method: - -```php -use App\Events\OrderShipmentStatusUpdated; - -broadcast(new OrderShipmentStatusUpdated($update))->via('pusher'); -``` - -Alternatively, you may specify the event's broadcast connection by calling the `broadcastVia` method within the event's constructor. However, before doing so, you should ensure that the event class uses the `InteractsWithBroadcasting` trait: - -```php -broadcastVia('pusher'); - } -} -``` - -## Receiving Broadcasts - -### Listening for Events - -Once you have [installed and instantiated Laravel Echo](/docs/broadcasting#client-side-installation), you are ready to start listening for events that are broadcast from your Laravel application. First, use the `channel` method to retrieve an instance of a channel, then call the `listen` method to listen for a specified event: - -```js -Echo.channel(`orders.${this.order.id}`) - .listen('OrderShipmentStatusUpdated', (e) => { - console.log(e.order.name); - }); -``` - -If you would like to listen for events on a private channel, use the `private` method instead. You may continue to chain calls to the `listen` method to listen for multiple events on a single channel: - -```js -Echo.private(`orders.${this.order.id}`) - .listen(/* ... */) - .listen(/* ... */) - .listen(/* ... */); -``` - -#### Stop Listening for Events - -If you would like to stop listening to a given event without [leaving the channel](/docs/broadcasting#leaving-a-channel), you may use the `stopListening` method: - -```js -Echo.private(`orders.${this.order.id}`) - .stopListening('OrderShipmentStatusUpdated') -``` - -### Leaving a Channel - -To leave a channel, you may call the `leaveChannel` method on your Echo instance: - -```js -Echo.leaveChannel(`orders.${this.order.id}`); -``` - -If you would like to leave a channel and also its associated private and presence channels, you may call the `leave` method: - -```js -Echo.leave(`orders.${this.order.id}`); -``` - -### Namespaces - -You may have noticed in the examples above that we did not specify the full `App\Events` namespace for the event classes. This is because Echo will automatically assume the events are located in the `App\Events` namespace. However, you may configure the root namespace when you instantiate Echo by passing a `namespace` configuration option: - -```js -window.Echo = new Echo({ - broadcaster: 'pusher', - // ... - namespace: 'App.Other.Namespace' -}); -``` - -Alternatively, you may prefix event classes with a `.` when subscribing to them using Echo. This will allow you to always specify the fully-qualified class name: - -```js -Echo.channel('orders') - .listen('.Namespace\\Event\\Class', (e) => { - // ... - }); -``` - -## Presence Channels - -Presence channels build on the security of private channels while exposing the additional feature of awareness of who is subscribed to the channel. This makes it easy to build powerful, collaborative application features such as notifying users when another user is viewing the same page or listing the inhabitants of a chat room. - -### Authorizing Presence Channels - -All presence channels are also private channels; therefore, users must be [authorized to access them](/docs/broadcasting#authorizing-channels). However, when defining authorization callbacks for presence channels, you will not return `true` if the user is authorized to join the channel. Instead, you should return an array of data about the user. - -The data returned by the authorization callback will be made available to the presence channel event listeners in your JavaScript application. If the user is not authorized to join the presence channel, you should return `false` or `null`: - -```php -use App\Models\User; - -Broadcast::channel('chat.{roomId}', function (User $user, int $roomId) { - if ($user->canJoinRoom($roomId)) { - return ['id' => $user->id, 'name' => $user->name]; - } -}); -``` - -### Joining Presence Channels - -To join a presence channel, you may use Echo's `join` method. The `join` method will return a `PresenceChannel` implementation which, along with exposing the `listen` method, allows you to subscribe to the `here`, `joining`, and `leaving` events. - -```js -Echo.join(`chat.${roomId}`) - .here((users) => { - // ... - }) - .joining((user) => { - console.log(user.name); - }) - .leaving((user) => { - console.log(user.name); - }) - .error((error) => { - console.error(error); - }); -``` - -The `here` callback will be executed immediately once the channel is joined successfully, and will receive an array containing the user information for all of the other users currently subscribed to the channel. The `joining` method will be executed when a new user joins a channel, while the `leaving` method will be executed when a user leaves the channel. The `error` method will be executed when the authentication endpoint returns an HTTP status code other than 200 or if there is a problem parsing the returned JSON. - -### Broadcasting to Presence Channels - -Presence channels may receive events just like public or private channels. Using the example of a chatroom, we may want to broadcast `NewMessage` events to the room's presence channel. To do so, we'll return an instance of `PresenceChannel` from the event's `broadcastOn` method: - -```php -/** - * Get the channels the event should broadcast on. - * - * @return array - */ -public function broadcastOn(): array -{ - return [ - new PresenceChannel('chat.'.$this->message->room_id), - ]; -} -``` - -As with other events, you may use the `broadcast` helper and the `toOthers` method to exclude the current user from receiving the broadcast: - -```php -broadcast(new NewMessage($message)); - -broadcast(new NewMessage($message))->toOthers(); -``` - -As typical of other types of events, you may listen for events sent to presence channels using Echo's `listen` method: - -```js -Echo.join(`chat.${roomId}`) - .here(/* ... */) - .joining(/* ... */) - .leaving(/* ... */) - .listen('NewMessage', (e) => { - // ... - }); -``` - -## Model Broadcasting - -::: important -Before reading the following documentation about model broadcasting, we recommend you become familiar with the general concepts of Hypervel's model broadcasting services as well as how to manually create and listen to broadcast events. -::: - -It is common to broadcast events when your application's [Eloquent](/docs/eloquent) models are created, updated, or deleted. Of course, this can easily be accomplished by manually [defining custom events for Eloquent model state changes](/docs/eloquent#events) and marking those events with the `ShouldBroadcast` interface. - -However, if you are not using these events for any other purposes in your application, it can be cumbersome to create event classes for the sole purpose of broadcasting them. To remedy this, Laravel allows you to indicate that an Eloquent model should automatically broadcast its state changes. - -To get started, your Eloquent model should use the `Hypervel\Database\Eloquent\BroadcastsEvents` trait. In addition, the model should define a `broadcastOn` method, which will return an array of channels that the model's events should broadcast on: - -```php -belongsTo(User::class); - } - - /** - * Get the channels that model events should broadcast on. - * - * @return array - */ - public function broadcastOn(string $event): array - { - return [$this, $this->user]; - } -} -``` - -Once your model includes this trait and defines its broadcast channels, it will begin automatically broadcasting events when a model instance is created, updated, deleted, trashed, or restored. - -In addition, you may have noticed that the `broadcastOn` method receives a string `$event` argument. This argument contains the type of event that has occurred on the model and will have a value of `created`, `updated` or `deleted`. By inspecting the value of this variable, you may determine which channels (if any) the model should broadcast to for a particular event: - -```php -/** - * Get the channels that model events should broadcast on. - * - * @return array> - */ -public function broadcastOn(string $event): array -{ - return match ($event) { - 'deleted' => [], - default => [$this, $this->user], - }; -} -``` - -#### Customizing Model Broadcasting Event Creation - -Occasionally, you may wish to customize how Hypervel creates the underlying model broadcasting event. You may accomplish this by defining a `newBroadcastableEvent` method on your Eloquent model. This method should return an `Hypervel\Database\Eloquent\BroadcastableModelEventOccurred` instance: - -```php -use Hypervel\Database\Eloquent\BroadcastableModelEventOccurred; - -/** - * Create a new broadcastable model event for the model. - */ -protected function newBroadcastableEvent(string $event): BroadcastableModelEventOccurred -{ - return (new BroadcastableModelEventOccurred( - $this, $event - ))->dontBroadcastToCurrentUser(); -} -``` - -### Model Broadcasting Conventions - -#### Channel Conventions - -As you may have noticed, the `broadcastOn` method in the model example above did not return `Channel` instances. Instead, Eloquent models were returned directly. If an Eloquent model instance is returned by your model's `broadcastOn` method (or is contained in an array returned by the method), Hypervel will automatically instantiate a private channel instance for the model using the model's class name and primary key identifier as the channel name. - -So, an `App\Models\User` model with an id of 1 would be converted into an `Hypervel\Broadcasting\PrivateChannel` instance with a name of `App.Models.User.1`. Of course, in addition to returning Eloquent model instances from your model's `broadcastOn` method, you may return complete `Channel` instances in order to have full control over the model's channel names: - -```php -use Hypervel\Broadcasting\PrivateChannel; - -/** - * Get the channels that model events should broadcast on. - * - * @return array - */ -public function broadcastOn(string $event): array -{ - return [ - new PrivateChannel('user.'.$this->id) - ]; -} -``` - -If you plan to explicitly return a channel instance from your model's `broadcastOn` method, you may pass an Eloquent model instance to the channel's constructor. When doing so, Hypervel will use the model channel conventions discussed above to convert the Eloquent model into a channel name string: - -```php -return [new Channel($this->user)]; -``` - -If you need to determine the channel name of a model, you may call the `broadcastChannel` method on any model instance. For example, this method returns the string `App.Models.User.1` for an `App\Models\User` model with an id of 1: - -```php -$user->broadcastChannel() -``` - -#### Event Conventions - -Since model broadcast events are not associated with an "actual" event within your application's `App\Events` directory, they are assigned a name and a payload based on conventions. Hypervel's convention is to broadcast the event using the class name of the model (not including the namespace) and the name of the model event that triggered the broadcast. - -So, for example, an update to the `App\Models\Post` model would broadcast an event to your client-side application as `PostUpdated` with the following payload: - -```json -{ - "model": { - "id": 1, - "title": "My first post" - ... - }, - ... - "socket": "someSocketId", -} -``` - -The deletion of the `App\Models\User` model would broadcast an event named UserDeleted. - -If you would like, you may define a custom broadcast name and payload by adding a `broadcastAs` and `broadcastWith` method to your model. These methods receive the name of the model event / operation that is occurring, allowing you to customize the event's name and payload for each model operation. If `null` is returned from the `broadcastAs` method, Hypervel will use the model broadcasting event name conventions discussed above when broadcasting the event: - -```php -/** - * The model event's broadcast name. - */ -public function broadcastAs(string $event): string|null -{ - return match ($event) { - 'created' => 'post.created', - default => null, - }; -} - -/** - * Get the data to broadcast for the model. - * - * @return array - */ -public function broadcastWith(string $event): array -{ - return match ($event) { - 'created' => ['title' => $this->title], - default => ['model' => $this], - }; -} -``` - -### Listening for Model Broadcasts - -Once you have added the `BroadcastsEvents` trait to your model and defined your model's `broadcastOn` method, you are ready to start listening for broadcasted model events within your client-side application. Before getting started, you may wish to consult the complete documentation on [listening for events](/docs/broadcasting#listening-for-events). - -First, use the `private` method to retrieve an instance of a channel, then call the `listen` method to listen for a specified event. Typically, the channel name given to the `private` method should correspond to Hypervel's [model broadcasting conventions](/docs/broadcasting#model-broadcasting-conventions). - -Once you have obtained a channel instance, you may use the `listen` method to listen for a particular event. Since model broadcast events are not associated with an "actual" event within your application's `App\Events` directory, the [event name](/docs/broadcasting#model-broadcasting-event-conventions) must be prefixed with a . to indicate it does not belong to a particular namespace. Each model broadcast event has a `model` property which contains all of the broadcastable properties of the model: - -```php -Echo.private(`App.Models.User.${this.user.id}`) - .listen('.PostUpdated', (e) => { - console.log(e.model); - }); -``` - -## Client Events - -::: tip -When using [Pusher Channels](https://pusher.com/channels), you must enable the "Client Events" option in the "App Settings" section of your [application dashboard](https://dashboard.pusher.com) in order to send client events. -::: - -Sometimes you may wish to broadcast an event to other connected clients without hitting your Hypervel application at all. This can be particularly useful for things like "typing" notifications, where you want to alert users of your application that another user is typing a message on a given screen. - -To broadcast client events, you may use Echo's `whisper` method: - -```js -Echo.private(`chat.${roomId}`) - .whisper('typing', { - name: this.user.name - }); -``` - -To listen for client events, you may use the `listenForWhisper` method: - -```js -Echo.private(`chat.${roomId}`) - .listenForWhisper('typing', (e) => { - console.log(e.name); - }); -``` - -## Notifications - -By pairing event broadcasting with [notifications](/docs/notifications), your JavaScript application may receive new notifications as they occur without needing to refresh the page. Before getting started, be sure to read over the documentation on using the [broadcast notification channel](/docs/notifications#broadcast-notifications). - -Once you have configured a notification to use the broadcast channel, you may listen for the broadcast events using Echo's notification method. Remember, the channel name should match the class name of the entity receiving the notifications: - -```js -Echo.private(`App.Models.User.${userId}`) - .notification((notification) => { - console.log(notification.type); - }); -``` - -In this example, all notifications sent to user model's channel would be received by the callback. A channel authorization callback for the `App.Models.User.{id}` channel is included in the default `BroadcastServiceProvider` that ships with the Hypervel framework. \ No newline at end of file diff --git a/_archive/docs/0.3/docs/cache.md b/_archive/docs/0.3/docs/cache.md deleted file mode 100644 index 80ab6179c6..0000000000 --- a/_archive/docs/0.3/docs/cache.md +++ /dev/null @@ -1,468 +0,0 @@ -# Cache -[[toc]] - -## Introduction - -Some of the data retrieval or processing tasks performed by your application could be CPU intensive or take several seconds to complete. When this is the case, it is common to cache the retrieved data for a time so it can be retrieved quickly on subsequent requests for the same data. The cached data is usually stored in a very fast data store such as [Redis](https://redis.io). - -Thankfully, Hypervel provides an expressive, unified API for various cache backends, allowing you to take advantage of their blazing fast data retrieval and speed up your web application. - -::: tip -Because Hypervel implements the same cache protocol as Laravel, you can share the cache and locks between the frameworks -::: - -## Configuration - -Your application's cache configuration file is located at `config/cache.php`. In this file, you may specify which cache driver you would like to be used by default throughout your application. Hypervel supports popular caching backends like [Redis](https://redis.io), relational databases and Swoole Table out of the box. Swoole Table out of the box. In addition, a file based cache driver is available, while `array` and "null" cache drivers provide convenient cache backends for your automated tests. - -The cache configuration file also contains various other options, which are documented within the file, so make sure to read over these options. By default, Hypervel is configured to use the `redis` cache driver, which stores the serialized, cached objects on the redis server. For larger applications, it is recommended that you keep using Redis. You may even configure multiple cache configurations for the same driver. - -### Driver Prerequisites - -#### Database - -When using the `database` cache driver, you will need a database table to contain the cache data. Before using database driver, you need to create `0001_01_01_000001_create_cache_table.php` [database migration](/docs/migrations) via `make:cache-table` Artisan command: - -```shell -php artisan make:cache-table - -php artisan migrate -``` - -#### Redis - -Before using a Redis cache with Hypervel, you will need to either install the PhpRedis PHP extension via PECL. - -## Cache Usage - -### Obtaining a Cache Instance - -To obtain a cache store instance, you may use the `Cache` facade, which is what we will use throughout this documentation. The `Cache` facade provides convenient, terse access to the underlying implementations of the Hypervel cache contracts: - -```php -get('foo'); - -Cache::store('redis')->put('bar', 'baz', 600); // 10 Minutes -``` - -### Retrieving Items From the Cache - -The `Cache` facade's `get` method is used to retrieve items from the cache. If the item does not exist in the cache, `null` will be returned. If you wish, you may pass a second argument to the `get` method specifying the default value you wish to be returned if the item doesn't exist: - -```php -$value = Cache::get('key'); - -$value = Cache::get('key', 'default'); -``` - -You may even pass a closure as the default value. The result of the closure will be returned if the specified item does not exist in the cache. Passing a closure allows you to defer the retrieval of default values from a database or other external service: - -```php -$value = Cache::get('key', function () { - return DB::table(/* ... */)->get(); -}); -``` - -#### Determining Item Existence - -The `has` method may be used to determine if an item exists in the cache. This method will also return `false` if the item exists but its value is `null`: - -```php -if (Cache::has('key')) { - // ... -} -``` - -#### Incrementing / Decrementing Values - -The `increment` and `decrement` methods may be used to adjust the value of integer items in the cache. Both of these methods accept an optional second argument indicating the amount by which to increment or decrement the item's value: - -```php -// Initialize the value if it does not exist... -Cache::add('key', 0, now()->addHours(4)); - -// Increment or decrement the value... -Cache::increment('key'); -Cache::increment('key', $amount); -Cache::decrement('key'); -Cache::decrement('key', $amount); -``` - -#### Retrieve and Store - -Sometimes you may wish to retrieve an item from the cache, but also store a default value if the requested item doesn't exist. For example, you may wish to retrieve all users from the cache or, if they don't exist, retrieve them from the database and add them to the cache. You may do this using the `Cache::remember` method: - -```php -$value = Cache::remember('users', $seconds, function () { - return DB::table('users')->get(); -}); -``` - -If the item does not exist in the cache, the closure passed to the `remember` method will be executed and its result will be placed in the cache. - -You may use the `rememberForever` method to retrieve an item from the cache or store it forever if it does not exist: - -```php -$value = Cache::rememberForever('users', function () { - return DB::table('users')->get(); -}); -``` - -#### Retrieve and Delete - -If you need to retrieve an item from the cache and then delete the item, you may use the `pull` method. Like the `get` method, `null` will be returned if the item does not exist in the cache: - -```php -$value = Cache::pull('key'); -``` - -### Storing Items in the Cache - -You may use the `put` method on the `Cache` facade to store items in the cache: - -```php -Cache::put('key', 'value', $seconds = 10); -``` - -If the storage time is not passed to the `put` method, the item will be stored indefinitely: - -```php -Cache::put('key', 'value'); -``` - -Instead of passing the number of seconds as an integer, you may also pass a `DateTime` instance representing the desired expiration time of the cached item: - -```php -Cache::put('key', 'value', now()->addMinutes(10)); -``` - -#### Store if Not Present - -The `add` method will only add the item to the cache if it does not already exist in the cache store. The method will return `true` if the item is actually added to the cache. Otherwise, the method will return `false`. The `add` method is an atomic operation: - -```php -Cache::add('key', 'value', $seconds); -``` - -#### Storing Items Forever - -The `forever` method may be used to store an item in the cache permanently. Since these items will not expire, they must be manually removed from the cache using the `forget` method: - -```php -Cache::forever('key', 'value'); -``` - -### Removing Items From the Cache - -You may remove items from the cache using the `forget` method: - -```php -Cache::forget('key'); -``` - -You may also remove items by providing a zero or negative number of expiration seconds: - -```php -Cache::put('key', 'value', 0); - -Cache::put('key', 'value', -5); -``` - -You may clear the entire cache using the `flush` method: - -```php -Cache::flush(); -``` - -::: warning -Flushing the cache does not respect your configured cache "prefix" and will remove all entries from the cache. Consider this carefully when clearing a cache which is shared by other applications. -::: - -### Building Cache Stacks - -Hypervel provides multi-tier caching architecture. The `stack` driver allows you to combine multiple cache layers for ultra performance. To illustrate how to use cache stacks, let's take a look at an example configuration that you might see in a production application: - -```php -'stack' => [ - 'driver' => 'stack', - 'stores' => [ - 'swoole' => [ - 'ttl' => 3, // seconds - ], - 'redis', - ], -], -``` - -Let's dissect this configuration. First, notice our `stack` aggregates two other cache drivers via its option: `swoole` and `redis`. Consequently, when caching data, both of these cache drivers will be invoked sequentially. The `ttl` option can specify the Time to Live (TTL) for each driver. - -In this configuration, if there's a cache hit in the `swoole` layer, the data will be returned immediately, and the `redis` cache will not be queried. However, if there's a cache miss in the `swoole` layer, the stack driver will proceed to check subsequent drivers to determine if the cache exists in those layers. Should a cache hit occur in the `redis` driver, the data will be returned from that layer and simultaneously cached in the `swoole` driver for future use. - -### The Cache Helper - -In addition to using the `Cache` facade, you may also use the global `cache` function to retrieve and store data via the cache. When the `cache` function is called with a single, string argument, it will return the value of the given key: - -```php -$value = cache('key'); -``` - -If you provide an array of key / value pairs and an expiration time to the function, it will store values in the cache for the specified duration: - -```php -cache(['key' => 'value'], $seconds); - -cache(['key' => 'value'], now()->addMinutes(10)); -``` - -When the `cache` function is called without any arguments, it returns an instance of the `Hypervel\Cache\Contracts\Factory` implementation, allowing you to call other caching methods: - -```php -cache()->remember('users', $seconds, function () { - return DB::table('users')->get(); -}); -``` - -::: tip -When testing call to the global `cache` function, you may use the `Cache::shouldReceive` method just as if you were [testing the facade](/docs/mocking#mocking-facades). -::: - -## Atomic Locks - -::: warning -To utilize this feature, your application must be using the `redis`, `file`, `database` or `array` cache driver as your application's default cache driver. In addition, all servers must be communicating with the same central cache server. -::: - -### Managing Locks - -Atomic locks allow for the manipulation of distributed locks without worrying about race conditions. You may create and manage locks using the `Cache::lock` method: - -```php -use Hypervel\Support\Facades\Cache; - -$lock = Cache::lock('foo', 10); - -if ($lock->get()) { - // Lock acquired for 10 seconds... - - $lock->release(); -} -``` - -The `get` method also accepts a closure. After the closure is executed, Hypervel will automatically release the lock: - -```php -Cache::lock('foo', 10)->get(function () { - // Lock acquired for 10 seconds and automatically released... -}); -``` - -If the lock is not available at the moment you request it, you may instruct Hypervel to wait for a specified number of seconds. If the lock can not be acquired within the specified time limit, an `Hypervel\Cache\Exceptions\LockTimeoutException` will be thrown: - -```php -use Hypervel\Cache\Exceptions\LockTimeoutException; - -$lock = Cache::lock('foo', 10); - -try { - $lock->block(5); - - // Lock acquired after waiting a maximum of 5 seconds... -} catch (LockTimeoutException $e) { - // Unable to acquire lock... -} finally { - $lock?->release(); -} -``` - -The example above may be simplified by passing a closure to the `block` method. When a closure is passed to this method, Hypervel will attempt to acquire the lock for the specified number of seconds and will automatically release the lock once the closure has been executed: - -```php -Cache::lock('foo', 10)->block(5, function () { - // Lock acquired after waiting a maximum of 5 seconds... -}); -``` - -### Managing Locks Across Processes - -Sometimes, you may wish to acquire a lock in one process and release it in another process. For example, you may acquire a lock during a web request and wish to release the lock at the end of a queued job that is triggered by that request. In this scenario, you should pass the lock's scoped "owner token" to the queued job so that the job can re-instantiate the lock using the given token. - -In the example below, we will execute a task if a lock is successfully acquired. In addition, we will pass the lock's owner token to the task via the lock's `owner` method: - -```php -use Hyperf\Task\TaskExecutor; -use Hyperf\Task\Task; - -$podcast = Podcast::find($id); - -$lock = Cache::lock('processing', 120); - -if ($lock->get()) { - app(TaskExecutor::class) - ->execute(new Task( - [MethodTask::class, 'handle'], - $lock->owner() - ); -} -``` - -Within our application's `MethodTask` task, we can restore and release the lock using the owner token: - -```php -class MethodTask -{ - public function handle($lockOwner) - { - $lock = Cache::restoreLock('processing', $lockOwner); - $lock->release(); - } -} -``` - -If you would like to release a lock without respecting its current owner, you may use the `forceRelease` method: - -```php -Cache::lock('processing')->forceRelease(); -``` - -## Adding Custom Cache Drivers - -### Writing the Driver - -To create our custom cache driver, we first need to implement the `Hypervel\Cache\Contracts\Store` [contract](/docs/contracts). So, a MongoDB cache implementation might look something like this: - -```php -app->booting(function () { - Cache::extend('mongo', function (Application $app) { - return Cache::repository(new MongoStore); - }); - }); - } - - /** - * Bootstrap any application services. - */ - public function boot(): void - { - // ... - } -} -``` - -The first argument passed to the `extend` method is the name of the driver. This will correspond to your `driver` option in the `config/cache.php` configuration file. The second argument is a closure that should return an `Hypervel\Cache\Repository` instance. The closure will be passed an `$app` instance, which is an instance of the [service container](/docs/container). - -Once your extension is registered, update your `config/cache.php` configuration file's `driver` option to the name of your extension. - -## Events - -To execute code on every cache operation, you may listen for various [events](/docs/events) dispatched by the cache: - -
    - -| Event Name | -|----------------------------------------------| -| `Hypervel\Cache\Events\CacheFlushed` | -| `Hypervel\Cache\Events\CacheFlushing` | -| `Hypervel\Cache\Events\CacheHit` | -| `Hypervel\Cache\Events\CacheMissed` | -| `Hypervel\Cache\Events\ForgettingKey` | -| `Hypervel\Cache\Events\KeyForgetFailed` | -| `Hypervel\Cache\Events\KeyForgotten` | -| `Hypervel\Cache\Events\KeyWriteFailed` | -| `Hypervel\Cache\Events\KeyWritten` | -| `Hypervel\Cache\Events\RetrievingKey` | -| `Hypervel\Cache\Events\RetrievingManyKeys` | -| `Hypervel\Cache\Events\WritingKey` | -| `Hypervel\Cache\Events\WritingManyKeys` | - -
    - -To increase performance, cache events are disabled in Hypervel by default. You may enable cache events by setting the `events` configuration option to `true` for a given cache store in your application's `config/cache.php` configuration file: - -```php -'database' => [ - 'driver' => 'database', - // ... - 'events' => true, -], -``` diff --git a/_archive/docs/0.3/docs/collections.md b/_archive/docs/0.3/docs/collections.md deleted file mode 100644 index 7d073a65c2..0000000000 --- a/_archive/docs/0.3/docs/collections.md +++ /dev/null @@ -1,4084 +0,0 @@ -# Collections -[[toc]] - -## Introduction - -The `Hypervel\Support\Collection` class provides a fluent, convenient wrapper for working with arrays of data. For example, check out the following code. We'll use the `collect` helper to create a new collection instance from the array, run the `strtoupper` function on each element, and then remove all empty elements: - -```php -$collection = collect(['taylor', 'abigail', null])->map(function (?string $name) { - return strtoupper($name); -})->reject(function (string $name) { - return empty($name); -}); -``` - -As you can see, the `Collection` class allows you to chain its methods to perform fluent mapping and reducing of the underlying array. In general, collections are immutable, meaning every `Collection` method returns an entirely new `Collection` instance. - -### Creating Collections - -As mentioned above, the `collect` helper returns a new `Hypervel\Support\Collection` instance for the given array. So, creating a collection is as simple as: - -```php -$collection = collect([1, 2, 3]); -``` - -::: note -The results of [Eloquent](/docs/eloquent) queries are always returned as `Collection` instances. -::: - -### Extending Collections - -Collections are "macroable", which allows you to add additional methods to the `Collection` class at run time. The `Hypervel\Support\Collection` class' `macro` method accepts a closure that will be executed when your macro is called. The macro closure may access the collection's other methods via `$this`, just as if it were a real method of the collection class. For example, the following code adds a `toUpper` method to the `Collection` class: - -```php -use Hypervel\Support\Collection; -use Hypervel\Support\Str; - -Collection::macro('toUpper', function () { - return $this->map(function (string $value) { - return Str::upper($value); - }); -}); - -$collection = collect(['first', 'second']); - -$upper = $collection->toUpper(); - -// ['FIRST', 'SECOND'] -``` - -Typically, you should declare collection macros in the `boot` method of a [service provider](/docs/providers). - -#### Macro Arguments - -If necessary, you may define macros that accept additional arguments: - -```php -use Hypervel\Support\Collection; -use Hypervel\Support\Facades\Lang; - -Collection::macro('toLocale', function (string $locale) { - return $this->map(function (string $value) use ($locale) { - return Lang::get($value, [], $locale); - }); -}); - -$collection = collect(['first', 'second']); - -$translated = $collection->toLocale('es'); -``` - -## Available Methods - -For the majority of the remaining collection documentation, we'll discuss each method available on the `Collection` class. Remember, all of these methods may be chained to fluently manipulate the underlying array. Furthermore, almost every method returns a new `Collection` instance, allowing you to preserve the original copy of the collection when necessary: - - - -
    - -[all](#method-all) -[average](#method-average) -[avg](#method-avg) -[chunk](#method-chunk) -[chunkWhile](#method-chunkwhile) -[collapse](#method-collapse) -[collect](#method-collect) -[combine](#method-combine) -[concat](#method-concat) -[contains](#method-contains) -[containsOneItem](#method-containsoneitem) -[containsStrict](#method-containsstrict) -[count](#method-count) -[countBy](#method-countBy) -[crossJoin](#method-crossjoin) -[dd](#method-dd) -[diff](#method-diff) -[diffAssoc](#method-diffassoc) -[diffAssocUsing](#method-diffassocusing) -[diffKeys](#method-diffkeys) -[doesntContain](#method-doesntcontain) -[dot](#method-dot) -[dump](#method-dump) -[duplicates](#method-duplicates) -[duplicatesStrict](#method-duplicatesstrict) -[each](#method-each) -[eachSpread](#method-eachspread) -[ensure](#method-ensure) -[every](#method-every) -[except](#method-except) -[filter](#method-filter) -[first](#method-first) -[firstOrFail](#method-first-or-fail) -[firstWhere](#method-first-where) -[flatMap](#method-flatmap) -[flatten](#method-flatten) -[flip](#method-flip) -[forget](#method-forget) -[forPage](#method-forpage) -[get](#method-get) -[groupBy](#method-groupby) -[has](#method-has) -[hasAny](#method-hasany) -[implode](#method-implode) -[intersect](#method-intersect) -[intersectAssoc](#method-intersectAssoc) -[intersectByKeys](#method-intersectbykeys) -[isEmpty](#method-isempty) -[isNotEmpty](#method-isnotempty) -[join](#method-join) -[keyBy](#method-keyby) -[keys](#method-keys) -[last](#method-last) -[lazy](#method-lazy) -[macro](#method-macro) -[make](#method-make) -[map](#method-map) -[mapInto](#method-mapinto) -[mapSpread](#method-mapspread) -[mapToGroups](#method-maptogroups) -[mapWithKeys](#method-mapwithkeys) -[max](#method-max) -[median](#method-median) -[merge](#method-merge) -[mergeRecursive](#method-mergerecursive) -[min](#method-min) -[mode](#method-mode) -[nth](#method-nth) -[only](#method-only) -[pad](#method-pad) -[partition](#method-partition) -[percentage](#method-percentage) -[pipe](#method-pipe) -[pipeInto](#method-pipeinto) -[pipeThrough](#method-pipethrough) -[pluck](#method-pluck) -[pop](#method-pop) -[prepend](#method-prepend) -[pull](#method-pull) -[push](#method-push) -[put](#method-put) -[random](#method-random) -[range](#method-range) -[reduce](#method-reduce) -[reduceSpread](#method-reduce-spread) -[reject](#method-reject) -[replace](#method-replace) -[replaceRecursive](#method-replacerecursive) -[reverse](#method-reverse) -[search](#method-search) -[select](#method-select) -[shift](#method-shift) -[shuffle](#method-shuffle) -[skip](#method-skip) -[skipUntil](#method-skipuntil) -[skipWhile](#method-skipwhile) -[slice](#method-slice) -[sliding](#method-sliding) -[sole](#method-sole) -[some](#method-some) -[sort](#method-sort) -[sortBy](#method-sortby) -[sortByDesc](#method-sortbydesc) -[sortDesc](#method-sortdesc) -[sortKeys](#method-sortkeys) -[sortKeysDesc](#method-sortkeysdesc) -[sortKeysUsing](#method-sortkeysusing) -[splice](#method-splice) -[split](#method-split) -[splitIn](#method-splitin) -[sum](#method-sum) -[take](#method-take) -[takeUntil](#method-takeuntil) -[takeWhile](#method-takewhile) -[tap](#method-tap) -[times](#method-times) -[toArray](#method-toarray) -[toJson](#method-tojson) -[transform](#method-transform) -[undot](#method-undot) -[union](#method-union) -[unique](#method-unique) -[uniqueStrict](#method-uniquestrict) -[unless](#method-unless) -[unlessEmpty](#method-unlessempty) -[unlessNotEmpty](#method-unlessnotempty) -[unwrap](#method-unwrap) -[value](#method-value) -[values](#method-values) -[when](#method-when) -[whenEmpty](#method-whenempty) -[whenNotEmpty](#method-whennotempty) -[where](#method-where) -[whereStrict](#method-wherestrict) -[whereBetween](#method-wherebetween) -[whereIn](#method-wherein) -[whereInStrict](#method-whereinstrict) -[whereInstanceOf](#method-whereinstanceof) -[whereNotBetween](#method-wherenotbetween) -[whereNotIn](#method-wherenotin) -[whereNotInStrict](#method-wherenotinstrict) -[whereNotNull](#method-wherenotnull) -[whereNull](#method-wherenull) -[wrap](#method-wrap) -[zip](#method-zip) - -
    - -## Method Listing - - -#### `all()` {.collection-method .first-collection-method} - -The `all` method returns the underlying array represented by the collection: - -```php -collect([1, 2, 3])->all(); - -// [1, 2, 3] -``` - - -#### `average()` - -Alias for the [`avg`](#method-avg) method. - - -#### `avg()` - -The `avg` method returns the [average value](https://en.wikipedia.org/wiki/Average) of a given key: - -```php -$average = collect([ - ['foo' => 10], - ['foo' => 10], - ['foo' => 20], - ['foo' => 40] -])->avg('foo'); - -// 20 - -$average = collect([1, 1, 2, 4])->avg(); - -// 2 -``` - - -#### `chunk()` - -The `chunk` method breaks the collection into multiple, smaller collections of a given size: - -```php -$collection = collect([1, 2, 3, 4, 5, 6, 7]); - -$chunks = $collection->chunk(4); - -$chunks->all(); - -// [[1, 2, 3, 4], [5, 6, 7]] -``` - -This method is especially useful in [views](/docs/views) when working with a grid system such as [Bootstrap](https://getbootstrap.com/docs/4.1/layout/grid/). For example, imagine you have a collection of [Eloquent](/docs/eloquent) models you want to display in a grid: - -```html -@foreach ($products->chunk(3) as $chunk) -
    - @foreach ($chunk as $product) -
    {{ $product->name }}
    - @endforeach -
    -@endforeach -``` - - -#### `chunkWhile()` - -The `chunkWhile` method breaks the collection into multiple, smaller collections based on the evaluation of the given callback. The `$chunk` variable passed to the closure may be used to inspect the previous element: - -```php -$collection = collect(str_split('AABBCCCD')); - -$chunks = $collection->chunkWhile(function (string $value, int $key, Collection $chunk) { - return $value === $chunk->last(); -}); - -$chunks->all(); - -// [['A', 'A'], ['B', 'B'], ['C', 'C', 'C'], ['D']] -``` - - -#### `collapse()` - -The `collapse` method collapses a collection of arrays into a single, flat collection: - -```php -$collection = collect([ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9], -]); - -$collapsed = $collection->collapse(); - -$collapsed->all(); - -// [1, 2, 3, 4, 5, 6, 7, 8, 9] -``` - -#### `collect()` - -The `collect` method returns a new `Collection` instance with the items currently in the collection: - -```php -$collectionA = collect([1, 2, 3]); - -$collectionB = $collectionA->collect(); - -$collectionB->all(); - -// [1, 2, 3] -``` - -The `collect` method is primarily useful for converting [lazy collections](#lazy-collections) into standard `Collection` instances: - -```php -$lazyCollection = LazyCollection::make(function () { - yield 1; - yield 2; - yield 3; -}); - -$collection = $lazyCollection->collect(); - -$collection::class; - -// 'Hypervel\Support\Collection' - -$collection->all(); - -// [1, 2, 3] -``` - -::: note -The `collect` method is especially useful when you have an instance of `Enumerable` and need a non-lazy collection instance. Since `collect()` is part of the `Enumerable` contract, you can safely use it to get a `Collection` instance. -::: - - -#### `combine()` - -The `combine` method combines the values of the collection, as keys, with the values of another array or collection: - -```php -$collection = collect(['name', 'age']); - -$combined = $collection->combine(['George', 29]); - -$combined->all(); - -// ['name' => 'George', 'age' => 29] -``` - - -#### `concat()` - -The `concat` method appends the given `array` or collection's values onto the end of another collection: - -```php -$collection = collect(['John Doe']); - -$concatenated = $collection->concat(['Jane Doe'])->concat(['name' => 'Johnny Doe']); - -$concatenated->all(); - -// ['John Doe', 'Jane Doe', 'Johnny Doe'] -``` - -The `concat` method numerically reindexes keys for items concatenated onto the original collection. To maintain keys in associative collections, see the [merge](#method-merge) method. - - -#### `contains()` - -The `contains` method determines whether the collection contains a given item. You may pass a closure to the `contains` method to determine if an element exists in the collection matching a given truth test: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$collection->contains(function (int $value, int $key) { - return $value > 5; -}); - -// false -``` - -Alternatively, you may pass a string to the `contains` method to determine whether the collection contains a given item value: - -```php -$collection = collect(['name' => 'Desk', 'price' => 100]); - -$collection->contains('Desk'); - -// true - -$collection->contains('New York'); - -// false -``` - -You may also pass a key / value pair to the `contains` method, which will determine if the given pair exists in the collection: - -```php -$collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 100], -]); - -$collection->contains('product', 'Bookcase'); - -// false -``` - -The `contains` method uses "loose" comparisons when checking item values, meaning a string with an integer value will be considered equal to an integer of the same value. Use the [`containsStrict`](#method-containsstrict) method to filter using "strict" comparisons. - -For the inverse of `contains`, see the [doesntContain](#method-doesntcontain) method. - - -#### `containsOneItem()` - -The `containsOneItem` method determines whether the collection contains a single item: - -```php -collect([])->containsOneItem(); - -// false - -collect(['1'])->containsOneItem(); - -// true - -collect(['1', '2'])->containsOneItem(); - -// false -``` - - -#### `containsStrict()` - -This method has the same signature as the [`contains`](#method-contains) method; however, all values are compared using "strict" comparisons. - -::: note -This method's behavior is modified when using [Eloquent Collections](/docs/eloquent-collections#method-contains). -::: - - -#### `count()` - -The `count` method returns the total number of items in the collection: - -```php -$collection = collect([1, 2, 3, 4]); - -$collection->count(); - -// 4 -``` - - -#### `countBy()` - -The `countBy` method counts the occurrences of values in the collection. By default, the method counts the occurrences of every element, allowing you to count certain "types" of elements in the collection: - -```php -$collection = collect([1, 2, 2, 2, 3]); - -$counted = $collection->countBy(); - -$counted->all(); - -// [1 => 1, 2 => 3, 3 => 1] -``` - -You pass a closure to the `countBy` method to count all items by a custom value: - -```php -$collection = collect(['alice@gmail.com', 'bob@yahoo.com', 'carlos@gmail.com']); - -$counted = $collection->countBy(function (string $email) { - return substr(strrchr($email, "@"), 1); -}); - -$counted->all(); - -// ['gmail.com' => 2, 'yahoo.com' => 1] -``` - - -#### `crossJoin()` - -The `crossJoin` method cross joins the collection's values among the given arrays or collections, returning a Cartesian product with all possible permutations: - -```php -$collection = collect([1, 2]); - -$matrix = $collection->crossJoin(['a', 'b']); - -$matrix->all(); - -/* - [ - [1, 'a'], - [1, 'b'], - [2, 'a'], - [2, 'b'], - ] -*/ - -$collection = collect([1, 2]); - -$matrix = $collection->crossJoin(['a', 'b'], ['I', 'II']); - -$matrix->all(); - -/* - [ - [1, 'a', 'I'], - [1, 'a', 'II'], - [1, 'b', 'I'], - [1, 'b', 'II'], - [2, 'a', 'I'], - [2, 'a', 'II'], - [2, 'b', 'I'], - [2, 'b', 'II'], - ] -*/ -``` - - -#### `dd()` - -The `dd` method dumps the collection's items and ends execution of the script: - -```php -$collection = collect(['John Doe', 'Jane Doe']); - -$collection->dd(); - -/* - Collection { - #items: array:2 [ - 0 => "John Doe" - 1 => "Jane Doe" - ] - } -*/ -``` - -If you do not want to stop executing the script, use the [`dump`](#method-dump) method instead. - - -#### `diff()` - -The `diff` method compares the collection against another collection or a plain PHP `array` based on its values. This method will return the values in the original collection that are not present in the given collection: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$diff = $collection->diff([2, 4, 6, 8]); - -$diff->all(); - -// [1, 3, 5] -``` - -::: note -This method's behavior is modified when using [Eloquent Collections](/docs/eloquent-collections#method-diff). -::: - - -#### `diffAssoc()` - -The `diffAssoc` method compares the collection against another collection or a plain PHP `array` based on its keys and values. This method will return the key / value pairs in the original collection that are not present in the given collection: - -```php -$collection = collect([ - 'color' => 'orange', - 'type' => 'fruit', - 'remain' => 6, -]); - -$diff = $collection->diffAssoc([ - 'color' => 'yellow', - 'type' => 'fruit', - 'remain' => 3, - 'used' => 6, -]); - -$diff->all(); - -// ['color' => 'orange', 'remain' => 6] -``` - - -#### `diffAssocUsing()` - -Unlike `diffAssoc`, `diffAssocUsing` accepts a user supplied callback function for the indices comparison: - -```php -$collection = collect([ - 'color' => 'orange', - 'type' => 'fruit', - 'remain' => 6, -]); - -$diff = $collection->diffAssocUsing([ - 'Color' => 'yellow', - 'Type' => 'fruit', - 'Remain' => 3, -], 'strnatcasecmp'); - -$diff->all(); - -// ['color' => 'orange', 'remain' => 6] -``` - -The callback must be a comparison function that returns an integer less than, equal to, or greater than zero. For more information, refer to the PHP documentation on [`array_diff_uassoc`](https://www.php.net/array_diff_uassoc#refsect1-function.array-diff-uassoc-parameters), which is the PHP function that the `diffAssocUsing` method utilizes internally. - - -#### `diffKeys()` - -The `diffKeys` method compares the collection against another collection or a plain PHP `array` based on its keys. This method will return the key / value pairs in the original collection that are not present in the given collection: - -```php -$collection = collect([ - 'one' => 10, - 'two' => 20, - 'three' => 30, - 'four' => 40, - 'five' => 50, -]); - -$diff = $collection->diffKeys([ - 'two' => 2, - 'four' => 4, - 'six' => 6, - 'eight' => 8, -]); - -$diff->all(); - -// ['one' => 10, 'three' => 30, 'five' => 50] -``` - - -#### `doesntContain()` - -The `doesntContain` method determines whether the collection does not contain a given item. You may pass a closure to the `doesntContain` method to determine if an element does not exist in the collection matching a given truth test: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$collection->doesntContain(function (int $value, int $key) { - return $value < 5; -}); - -// false -``` - -Alternatively, you may pass a string to the `doesntContain` method to determine whether the collection does not contain a given item value: - -```php -$collection = collect(['name' => 'Desk', 'price' => 100]); - -$collection->doesntContain('Table'); - -// true - -$collection->doesntContain('Desk'); - -// false -``` - -You may also pass a key / value pair to the `doesntContain` method, which will determine if the given pair does not exist in the collection: - -```php -$collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 100], -]); - -$collection->doesntContain('product', 'Bookcase'); - -// true -``` - -The `doesntContain` method uses "loose" comparisons when checking item values, meaning a string with an integer value will be considered equal to an integer of the same value. - - -#### `dot()` - -The `dot` method flattens a multi-dimensional collection into a single level collection that uses "dot" notation to indicate depth: - -```php -$collection = collect(['products' => ['desk' => ['price' => 100]]]); - -$flattened = $collection->dot(); - -$flattened->all(); - -// ['products.desk.price' => 100] -``` - - -#### `dump()` - -The `dump` method dumps the collection's items: - -```php -$collection = collect(['John Doe', 'Jane Doe']); - -$collection->dump(); - -/* - Collection { - #items: array:2 [ - 0 => "John Doe" - 1 => "Jane Doe" - ] - } -*/ -``` - -If you want to stop executing the script after dumping the collection, use the [`dd`](#method-dd) method instead. - - -#### `duplicates()` - -The `duplicates` method retrieves and returns duplicate values from the collection: - -```php -$collection = collect(['a', 'b', 'a', 'c', 'b']); - -$collection->duplicates(); - -// [2 => 'a', 4 => 'b'] -``` - -If the collection contains arrays or objects, you can pass the key of the attributes that you wish to check for duplicate values: - -```php -$employees = collect([ - ['email' => 'abigail@example.com', 'position' => 'Developer'], - ['email' => 'james@example.com', 'position' => 'Designer'], - ['email' => 'victoria@example.com', 'position' => 'Developer'], -]); - -$employees->duplicates('position'); - -// [2 => 'Developer'] -``` - - -#### `duplicatesStrict()` - -This method has the same signature as the [`duplicates`](#method-duplicates) method; however, all values are compared using "strict" comparisons. - - -#### `each()` - -The `each` method iterates over the items in the collection and passes each item to a closure: - -```php -$collection = collect([1, 2, 3, 4]); - -$collection->each(function (int $item, int $key) { - // ... -}); -``` - -If you would like to stop iterating through the items, you may return `false` from your closure: - -```php -$collection->each(function (int $item, int $key) { - if (/* condition */) { - return false; - } -}); -``` - - -#### `eachSpread()` - -The `eachSpread` method iterates over the collection's items, passing each nested item value into the given callback: - -```php -$collection = collect([['John Doe', 35], ['Jane Doe', 33]]); - -$collection->eachSpread(function (string $name, int $age) { - // ... -}); -``` - -You may stop iterating through the items by returning `false` from the callback: - -```php -$collection->eachSpread(function (string $name, int $age) { - return false; -}); -``` - - -#### `ensure()` - -The `ensure` method may be used to verify that all elements of a collection are of a given type or list of types. Otherwise, an `UnexpectedValueException` will be thrown: - -```php -return $collection->ensure(User::class); - -return $collection->ensure([User::class, Customer::class]); -``` - -Primitive types such as `string`, `int`, `float`, `bool`, and `array` may also be specified: - -```php -return $collection->ensure('int'); -``` - -::: warning -The `ensure` method does not guarantee that elements of different types will not be added to the collection at a later time. -::: - - -#### `every()` - -The `every` method may be used to verify that all elements of a collection pass a given truth test: - -```php -collect([1, 2, 3, 4])->every(function (int $value, int $key) { - return $value > 2; -}); - -// false -``` - -If the collection is empty, the `every` method will return true: - -```php -$collection = collect([]); - -$collection->every(function (int $value, int $key) { - return $value > 2; -}); - -// true -``` - - -#### `except()` - -The `except` method returns all items in the collection except for those with the specified keys: - -```php -$collection = collect(['product_id' => 1, 'price' => 100, 'discount' => false]); - -$filtered = $collection->except(['price', 'discount']); - -$filtered->all(); - -// ['product_id' => 1] -``` - -For the inverse of `except`, see the [only](#method-only) method. - -::: note -This method's behavior is modified when using [Eloquent Collections](/docs/eloquent-collections#method-except). -::: - - -#### `filter()` - -The `filter` method filters the collection using the given callback, keeping only those items that pass a given truth test: - -```php -$collection = collect([1, 2, 3, 4]); - -$filtered = $collection->filter(function (int $value, int $key) { - return $value > 2; -}); - -$filtered->all(); - -// [3, 4] -``` - -If no callback is supplied, all entries of the collection that are equivalent to `false` will be removed: - -```php -$collection = collect([1, 2, 3, null, false, '', 0, []]); - -$collection->filter()->all(); - -// [1, 2, 3] -``` - -For the inverse of `filter`, see the [reject](#method-reject) method. - - -#### `first()` - -The `first` method returns the first element in the collection that passes a given truth test: - -```php -collect([1, 2, 3, 4])->first(function (int $value, int $key) { - return $value > 2; -}); - -// 3 -``` - -You may also call the `first` method with no arguments to get the first element in the collection. If the collection is empty, `null` is returned: - -```php -collect([1, 2, 3, 4])->first(); - -// 1 -``` - - -#### `firstOrFail()` - -The `firstOrFail` method is identical to the `first` method; however, if no result is found, an `Hyperf\Collection\ItemNotFoundException` exception will be thrown: - -```php -collect([1, 2, 3, 4])->firstOrFail(function (int $value, int $key) { - return $value > 5; -}); - -// Throws ItemNotFoundException... -``` - -You may also call the `firstOrFail` method with no arguments to get the first element in the collection. If the collection is empty, an `Hyperf\Collection\ItemNotFoundException` exception will be thrown: - -```php -collect([])->firstOrFail(); - -// Throws ItemNotFoundException... -``` - - -#### `firstWhere()` - -The `firstWhere` method returns the first element in the collection with the given key / value pair: - -```php -$collection = collect([ - ['name' => 'Regena', 'age' => null], - ['name' => 'Linda', 'age' => 14], - ['name' => 'Diego', 'age' => 23], - ['name' => 'Linda', 'age' => 84], -]); - -$collection->firstWhere('name', 'Linda'); - -// ['name' => 'Linda', 'age' => 14] -``` - -You may also call the `firstWhere` method with a comparison operator: - -```php -$collection->firstWhere('age', '>=', 18); - -// ['name' => 'Diego', 'age' => 23] -``` - -Like the [where](#method-where) method, you may pass one argument to the `firstWhere` method. In this scenario, the `firstWhere` method will return the first item where the given item key's value is "truthy": - - -```php -$collection->firstWhere('age'); - -// ['name' => 'Linda', 'age' => 14] -``` - - -#### `flatMap()` - -The `flatMap` method iterates through the collection and passes each value to the given closure. The closure is free to modify the item and return it, thus forming a new collection of modified items. Then, the array is flattened by one level: - -```php -$collection = collect([ - ['name' => 'Sally'], - ['school' => 'Arkansas'], - ['age' => 28] -]); - -$flattened = $collection->flatMap(function (array $values) { - return array_map('strtoupper', $values); -}); - -$flattened->all(); - -// ['name' => 'SALLY', 'school' => 'ARKANSAS', 'age' => '28']; -``` - - -#### `flatten()` - -The `flatten` method flattens a multi-dimensional collection into a single dimension: - -```php -$collection = collect([ - 'name' => 'taylor', - 'languages' => [ - 'php', 'javascript' - ] -]); - -$flattened = $collection->flatten(); - -$flattened->all(); - -// ['taylor', 'php', 'javascript']; -``` - -If necessary, you may pass the `flatten` method a "depth" argument: - -```php -$collection = collect([ - 'Apple' => [ - [ - 'name' => 'iPhone 6S', - 'brand' => 'Apple' - ], - ], - 'Samsung' => [ - [ - 'name' => 'Galaxy S7', - 'brand' => 'Samsung' - ], - ], -]); - -$products = $collection->flatten(1); - -$products->values()->all(); - -/* - [ - ['name' => 'iPhone 6S', 'brand' => 'Apple'], - ['name' => 'Galaxy S7', 'brand' => 'Samsung'], - ] -*/ -``` - -In this example, calling `flatten` without providing the depth would have also flattened the nested arrays, resulting in `['iPhone 6S', 'Apple', 'Galaxy S7', 'Samsung']`. Providing a depth allows you to specify the number of levels nested arrays will be flattened. - - -#### `flip()` - -The `flip` method swaps the collection's keys with their corresponding values: - -```php -$collection = collect(['language' => 'php', 'framework' => 'Hypervel']); - -$flipped = $collection->flip(); - -$flipped->all(); - -// ['language' => 'php', 'Hypervel' => 'framework'] -``` - - -#### `forget()` - -The `forget` method removes an item from the collection by its key: - -```php -$collection = collect(['language' => 'php', 'framework' => 'Hypervel']); - -$collection->forget('language'); - -$collection->all(); - -// ['framework' => 'Hypervel'] -``` - -::: warning -Unlike most other collection methods, `forget` does not return a new modified collection; it modifies the collection it is called on. -::: - - -#### `forPage()` - -The `forPage` method returns a new collection containing the items that would be present on a given page number. The method accepts the page number as its first argument and the number of items to show per page as its second argument: - -```php -$collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9]); - -$chunk = $collection->forPage(2, 3); - -$chunk->all(); - -// [4, 5, 6] -``` - - -#### `get()` - -The `get` method returns the item at a given key. If the key does not exist, `null` is returned: - -```php -$collection = collect(['language' => 'php', 'framework' => 'Hypervel']); - -$value = $collection->get('language'); - -// php -``` - -You may optionally pass a default value as the second argument: - -```php -$collection = collect(['language' => 'php', 'framework' => 'Hypervel']); - -$value = $collection->get('age', 34); - -// 34 -``` - -You may even pass a callback as the method's default value. The result of the callback will be returned if the specified key does not exist: - -```php - $collection->get('email', function () { - return 'taylor@example.com'; - }); - - // taylor@example.com -``` - - -#### `groupBy()` - -The `groupBy` method groups the collection's items by a given key: - -```php -$collection = collect([ - ['account_id' => 'account-x10', 'product' => 'Chair'], - ['account_id' => 'account-x10', 'product' => 'Bookcase'], - ['account_id' => 'account-x11', 'product' => 'Desk'], -]); - -$grouped = $collection->groupBy('account_id'); - -$grouped->all(); - -/* - [ - 'account-x10' => [ - ['account_id' => 'account-x10', 'product' => 'Chair'], - ['account_id' => 'account-x10', 'product' => 'Bookcase'], - ], - 'account-x11' => [ - ['account_id' => 'account-x11', 'product' => 'Desk'], - ], - ] -*/ -``` - -Instead of passing a string `key`, you may pass a callback. The callback should return the value you wish to key the group by: - -```php -$grouped = $collection->groupBy(function (array $item, int $key) { - return substr($item['account_id'], -3); -}); - -$grouped->all(); - -/* - [ - 'x10' => [ - ['account_id' => 'account-x10', 'product' => 'Chair'], - ['account_id' => 'account-x10', 'product' => 'Bookcase'], - ], - 'x11' => [ - ['account_id' => 'account-x11', 'product' => 'Desk'], - ], - ] -*/ -``` - -Multiple grouping criteria may be passed as an array. Each array element will be applied to the corresponding level within a multi-dimensional array: - -```php -$data = new Collection([ - 10 => ['user' => 1, 'skill' => 1, 'roles' => ['Role_1', 'Role_3']], - 20 => ['user' => 2, 'skill' => 1, 'roles' => ['Role_1', 'Role_2']], - 30 => ['user' => 3, 'skill' => 2, 'roles' => ['Role_1']], - 40 => ['user' => 4, 'skill' => 2, 'roles' => ['Role_2']], -]); - -$result = $data->groupBy(['skill', function (array $item) { - return $item['roles']; -}], preserveKeys: true); - -/* -[ - 1 => [ - 'Role_1' => [ - 10 => ['user' => 1, 'skill' => 1, 'roles' => ['Role_1', 'Role_3']], - 20 => ['user' => 2, 'skill' => 1, 'roles' => ['Role_1', 'Role_2']], - ], - 'Role_2' => [ - 20 => ['user' => 2, 'skill' => 1, 'roles' => ['Role_1', 'Role_2']], - ], - 'Role_3' => [ - 10 => ['user' => 1, 'skill' => 1, 'roles' => ['Role_1', 'Role_3']], - ], - ], - 2 => [ - 'Role_1' => [ - 30 => ['user' => 3, 'skill' => 2, 'roles' => ['Role_1']], - ], - 'Role_2' => [ - 40 => ['user' => 4, 'skill' => 2, 'roles' => ['Role_2']], - ], - ], -]; -*/ -``` - - -#### `has()` - -The `has` method determines if a given key exists in the collection: - -```php -$collection = collect(['account_id' => 1, 'product' => 'Desk', 'amount' => 5]); - -$collection->has('product'); - -// true - -$collection->has(['product', 'amount']); - -// true - -$collection->has(['amount', 'price']); - -// false -``` - - -#### `hasAny()` - -The `hasAny` method determines whether any of the given keys exist in the collection: - -```php -$collection = collect(['account_id' => 1, 'product' => 'Desk', 'amount' => 5]); - -$collection->hasAny(['product', 'price']); - -// true - -$collection->hasAny(['name', 'price']); - -// false -``` - - -#### `implode()` - -The `implode` method joins items in a collection. Its arguments depend on the type of items in the collection. If the collection contains arrays or objects, you should pass the key of the attributes you wish to join, and the "glue" string you wish to place between the values: - -```php -$collection = collect([ - ['account_id' => 1, 'product' => 'Desk'], - ['account_id' => 2, 'product' => 'Chair'], -]); - -$collection->implode('product', ', '); - -// Desk, Chair -``` - -If the collection contains simple strings or numeric values, you should pass the "glue" as the only argument to the method: - -```php -collect([1, 2, 3, 4, 5])->implode('-'); - -// '1-2-3-4-5' -``` - -You may pass a closure to the `implode` method if you would like to format the values being imploded: - -```php -$collection->implode(function (array $item, int $key) { - return strtoupper($item['product']); -}, ', '); - -// DESK, CHAIR -``` - - -#### `intersect()` - -The `intersect` method removes any values from the original collection that are not present in the given `array` or collection. The resulting collection will preserve the original collection's keys: - -```php -$collection = collect(['Desk', 'Sofa', 'Chair']); - -$intersect = $collection->intersect(['Desk', 'Chair', 'Bookcase']); - -$intersect->all(); - -// [0 => 'Desk', 2 => 'Chair'] -``` - -::: note -This method's behavior is modified when using [Eloquent Collections](/docs/eloquent-collections#method-intersect). -::: - - -#### `intersectAssoc()` - -The `intersectAssoc` method compares the original collection against another collection or `array`, returning the key / value pairs that are present in all of the given collections: - -```php -$collection = collect([ - 'color' => 'red', - 'size' => 'M', - 'material' => 'cotton' -]); - -$intersect = $collection->intersectAssoc([ - 'color' => 'blue', - 'size' => 'M', - 'material' => 'polyester' -]); - -$intersect->all(); - -// ['size' => 'M'] -``` - - -#### `intersectByKeys()` - -The `intersectByKeys` method removes any keys and their corresponding values from the original collection that are not present in the given `array` or collection: - -```php -$collection = collect([ - 'serial' => 'UX301', 'type' => 'screen', 'year' => 2009, -]); - -$intersect = $collection->intersectByKeys([ - 'reference' => 'UX404', 'type' => 'tab', 'year' => 2011, -]); - -$intersect->all(); - -// ['type' => 'screen', 'year' => 2009] -``` - - -#### `isEmpty()` - -The `isEmpty` method returns `true` if the collection is empty; otherwise, `false` is returned: - -```php -collect([])->isEmpty(); - -// true -``` - - -#### `isNotEmpty()` - -The `isNotEmpty` method returns `true` if the collection is not empty; otherwise, `false` is returned: - -```php -collect([])->isNotEmpty(); - -// false -``` - - -#### `join()` - -The `join` method joins the collection's values with a string. Using this method's second argument, you may also specify how the final element should be appended to the string: - -```php -collect(['a', 'b', 'c'])->join(', '); // 'a, b, c' -collect(['a', 'b', 'c'])->join(', ', ', and '); // 'a, b, and c' -collect(['a', 'b'])->join(', ', ' and '); // 'a and b' -collect(['a'])->join(', ', ' and '); // 'a' -collect([])->join(', ', ' and '); // '' -``` - - -#### `keyBy()` - -The `keyBy` method keys the collection by the given key. If multiple items have the same key, only the last one will appear in the new collection: - -```php -$collection = collect([ - ['product_id' => 'prod-100', 'name' => 'Desk'], - ['product_id' => 'prod-200', 'name' => 'Chair'], -]); - -$keyed = $collection->keyBy('product_id'); - -$keyed->all(); - -/* - [ - 'prod-100' => ['product_id' => 'prod-100', 'name' => 'Desk'], - 'prod-200' => ['product_id' => 'prod-200', 'name' => 'Chair'], - ] -*/ -``` - -You may also pass a callback to the method. The callback should return the value to key the collection by: - -```php -$keyed = $collection->keyBy(function (array $item, int $key) { - return strtoupper($item['product_id']); -}); - -$keyed->all(); - -/* - [ - 'PROD-100' => ['product_id' => 'prod-100', 'name' => 'Desk'], - 'PROD-200' => ['product_id' => 'prod-200', 'name' => 'Chair'], - ] -*/ -``` - - -#### `keys()` - -The `keys` method returns all of the collection's keys: - -```php -$collection = collect([ - 'prod-100' => ['product_id' => 'prod-100', 'name' => 'Desk'], - 'prod-200' => ['product_id' => 'prod-200', 'name' => 'Chair'], -]); - -$keys = $collection->keys(); - -$keys->all(); - -// ['prod-100', 'prod-200'] -``` - - -#### `last()` - -The `last` method returns the last element in the collection that passes a given truth test: - -```php -collect([1, 2, 3, 4])->last(function (int $value, int $key) { - return $value < 3; -}); - -// 2 -``` - -You may also call the `last` method with no arguments to get the last element in the collection. If the collection is empty, `null` is returned: - -```php -collect([1, 2, 3, 4])->last(); - -// 4 -``` - - -#### `lazy()` - -The `lazy` method returns a new [`LazyCollection`](#lazy-collections) instance from the underlying array of items: - -```php -$lazyCollection = collect([1, 2, 3, 4])->lazy(); - -$lazyCollection::class; - -// Hyperf\Collection\LazyCollection - -$lazyCollection->all(); - -// [1, 2, 3, 4] -``` - -This is especially useful when you need to perform transformations on a huge `Collection` that contains many items: - -```php -$count = $hugeCollection - ->lazy() - ->where('country', 'FR') - ->where('balance', '>', '100') - ->count(); -``` - -By converting the collection to a `LazyCollection`, we avoid having to allocate a ton of additional memory. Though the original collection still keeps _its_ values in memory, the subsequent filters will not. Therefore, virtually no additional memory will be allocated when filtering the collection's results. - - -#### `macro()` - -The static `macro` method allows you to add methods to the `Collection` class at run time. Refer to the documentation on [extending collections](#extending-collections) for more information. - - -#### `make()` - -The static `make` method creates a new collection instance. See the [Creating Collections](#creating-collections) section. - - -#### `map()` - -The `map` method iterates through the collection and passes each value to the given callback. The callback is free to modify the item and return it, thus forming a new collection of modified items: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$multiplied = $collection->map(function (int $item, int $key) { - return $item * 2; -}); - -$multiplied->all(); - -// [2, 4, 6, 8, 10] -``` - -::: warning -> Like most other collection methods, `map` returns a new collection instance; it does not modify the collection it is called on. If you want to transform the original collection, use the [`transform`](#method-transform) method. -::: - - -#### `mapInto()` - -The `mapInto()` method iterates over the collection, creating a new instance of the given class by passing the value into the constructor: - -```php -class Currency -{ - /** - * Create a new currency instance. - */ - function __construct( - public string $code - ) {} -} - -$collection = collect(['USD', 'EUR', 'GBP']); - -$currencies = $collection->mapInto(Currency::class); - -$currencies->all(); - -// [Currency('USD'), Currency('EUR'), Currency('GBP')] -``` - - -#### `mapSpread()` - -The `mapSpread` method iterates over the collection's items, passing each nested item value into the given closure. The closure is free to modify the item and return it, thus forming a new collection of modified items: - -```php -$collection = collect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); - -$chunks = $collection->chunk(2); - -$sequence = $chunks->mapSpread(function (int $even, int $odd) { - return $even + $odd; -}); - -$sequence->all(); - -// [1, 5, 9, 13, 17] -``` - - -#### `mapToGroups()` - -The `mapToGroups` method groups the collection's items by the given closure. The closure should return an associative array containing a single key / value pair, thus forming a new collection of grouped values: - -```php -$collection = collect([ - [ - 'name' => 'John Doe', - 'department' => 'Sales', - ], - [ - 'name' => 'Jane Doe', - 'department' => 'Sales', - ], - [ - 'name' => 'Johnny Doe', - 'department' => 'Marketing', - ] -]); - -$grouped = $collection->mapToGroups(function (array $item, int $key) { - return [$item['department'] => $item['name']]; -}); - -$grouped->all(); - -/* - [ - 'Sales' => ['John Doe', 'Jane Doe'], - 'Marketing' => ['Johnny Doe'], - ] -*/ - -$grouped->get('Sales')->all(); - -// ['John Doe', 'Jane Doe'] -``` - - -#### `mapWithKeys()` - -The `mapWithKeys` method iterates through the collection and passes each value to the given callback. The callback should return an associative array containing a single key / value pair: - -```php -$collection = collect([ - [ - 'name' => 'John', - 'department' => 'Sales', - 'email' => 'john@example.com', - ], - [ - 'name' => 'Jane', - 'department' => 'Marketing', - 'email' => 'jane@example.com', - ] -]); - -$keyed = $collection->mapWithKeys(function (array $item, int $key) { - return [$item['email'] => $item['name']]; -}); - -$keyed->all(); - -/* - [ - 'john@example.com' => 'John', - 'jane@example.com' => 'Jane', - ] -*/ -``` - - -#### `max()` - -The `max` method returns the maximum value of a given key: - -```php -$max = collect([ - ['foo' => 10], - ['foo' => 20] -])->max('foo'); - -// 20 - -$max = collect([1, 2, 3, 4, 5])->max(); - -// 5 -``` - - -#### `median()` - -The `median` method returns the [median value](https://en.wikipedia.org/wiki/Median) of a given key: - -```php -$median = collect([ - ['foo' => 10], - ['foo' => 10], - ['foo' => 20], - ['foo' => 40] -])->median('foo'); - -// 15 - -$median = collect([1, 1, 2, 4])->median(); - -// 1.5 -``` - - -#### `merge()` - -The `merge` method merges the given array or collection with the original collection. If a string key in the given items matches a string key in the original collection, the given item's value will overwrite the value in the original collection: - -```php -$collection = collect(['product_id' => 1, 'price' => 100]); - -$merged = $collection->merge(['price' => 200, 'discount' => false]); - -$merged->all(); - -// ['product_id' => 1, 'price' => 200, 'discount' => false] -``` - -If the given item's keys are numeric, the values will be appended to the end of the collection: - -```php -$collection = collect(['Desk', 'Chair']); - -$merged = $collection->merge(['Bookcase', 'Door']); - -$merged->all(); - -// ['Desk', 'Chair', 'Bookcase', 'Door'] -``` - - -#### `mergeRecursive()` - -The `mergeRecursive` method merges the given array or collection recursively with the original collection. If a string key in the given items matches a string key in the original collection, then the values for these keys are merged together into an array, and this is done recursively: - -```php -$collection = collect(['product_id' => 1, 'price' => 100]); - -$merged = $collection->mergeRecursive([ - 'product_id' => 2, - 'price' => 200, - 'discount' => false -]); - -$merged->all(); - -// ['product_id' => [1, 2], 'price' => [100, 200], 'discount' => false] -``` - - -#### `min()` - -The `min` method returns the minimum value of a given key: - -```php -$min = collect([['foo' => 10], ['foo' => 20]])->min('foo'); - -// 10 - -$min = collect([1, 2, 3, 4, 5])->min(); - -// 1 -``` - - -#### `mode()` - -The `mode` method returns the [mode value](https://en.wikipedia.org/wiki/Mode_(statistics)) of a given key: - -```php -$mode = collect([ - ['foo' => 10], - ['foo' => 10], - ['foo' => 20], - ['foo' => 40] -])->mode('foo'); - -// [10] - -$mode = collect([1, 1, 2, 4])->mode(); - -// [1] - -$mode = collect([1, 1, 2, 2])->mode(); - -// [1, 2] -``` - - -#### `nth()` - -The `nth` method creates a new collection consisting of every n-th element: - -```php -$collection = collect(['a', 'b', 'c', 'd', 'e', 'f']); - -$collection->nth(4); - -// ['a', 'e'] -``` - -You may optionally pass a starting offset as the second argument: - -```php -$collection->nth(4, 1); - -// ['b', 'f'] -``` - - -#### `only()` - -The `only` method returns the items in the collection with the specified keys: - -```php -$collection = collect([ - 'product_id' => 1, - 'name' => 'Desk', - 'price' => 100, - 'discount' => false -]); - -$filtered = $collection->only(['product_id', 'name']); - -$filtered->all(); - -// ['product_id' => 1, 'name' => 'Desk'] -``` - -For the inverse of `only`, see the [except](#method-except) method. - -::: note -This method's behavior is modified when using [Eloquent Collections](/docs/eloquent-collections#method-only). -::: - - -#### `pad()` - -The `pad` method will fill the array with the given value until the array reaches the specified size. This method behaves like the [array_pad](https://secure.php.net/manual/en/function.array-pad.php) PHP function. - -To pad to the left, you should specify a negative size. No padding will take place if the absolute value of the given size is less than or equal to the length of the array: - -```php -$collection = collect(['A', 'B', 'C']); - -$filtered = $collection->pad(5, 0); - -$filtered->all(); - -// ['A', 'B', 'C', 0, 0] - -$filtered = $collection->pad(-5, 0); - -$filtered->all(); - -// [0, 0, 'A', 'B', 'C'] -``` - - -#### `partition()` - -The `partition` method may be combined with PHP array destructuring to separate elements that pass a given truth test from those that do not: - -```php -$collection = collect([1, 2, 3, 4, 5, 6]); - -[$underThree, $equalOrAboveThree] = $collection->partition(function (int $i) { - return $i < 3; -}); - -$underThree->all(); - -// [1, 2] - -$equalOrAboveThree->all(); - -// [3, 4, 5, 6] -``` - - -#### `percentage()` - -The `percentage` method may be used to quickly determine the percentage of items in the collection that pass a given truth test: - -```php -$collection = collect([1, 1, 2, 2, 2, 3]); - -$percentage = $collection->percentage(fn ($value) => $value === 1); - -// 33.33 -``` - -By default, the percentage will be rounded to two decimal places. However, you may customize this behavior by providing a second argument to the method: - -```php -$percentage = $collection->percentage(fn ($value) => $value === 1, precision: 3); - -// 33.333 -``` - - -#### `pipe()` - -The `pipe` method passes the collection to the given closure and returns the result of the executed closure: - -```php -$collection = collect([1, 2, 3]); - -$piped = $collection->pipe(function (Collection $collection) { - return $collection->sum(); -}); - -// 6 -``` - - -#### `pipeInto()` - -The `pipeInto` method creates a new instance of the given class and passes the collection into the constructor: - -```php -class ResourceCollection -{ - /** - * Create a new ResourceCollection instance. - */ - public function __construct( - public Collection $collection, - ) {} -} - -$collection = collect([1, 2, 3]); - -$resource = $collection->pipeInto(ResourceCollection::class); - -$resource->collection->all(); - -// [1, 2, 3] -``` - - -#### `pipeThrough()` - -The `pipeThrough` method passes the collection to the given array of closures and returns the result of the executed closures: - -```php -use Hypervel\Support\Collection; - -$collection = collect([1, 2, 3]); - -$result = $collection->pipeThrough([ - function (Collection $collection) { - return $collection->merge([4, 5]); - }, - function (Collection $collection) { - return $collection->sum(); - }, -]); - -// 15 -``` - - -#### `pluck()` - -The `pluck` method retrieves all of the values for a given key: - -```php -$collection = collect([ - ['product_id' => 'prod-100', 'name' => 'Desk'], - ['product_id' => 'prod-200', 'name' => 'Chair'], -]); - -$plucked = $collection->pluck('name'); - -$plucked->all(); - -// ['Desk', 'Chair'] -``` - -You may also specify how you wish the resulting collection to be keyed: - -```php -$plucked = $collection->pluck('name', 'product_id'); - -$plucked->all(); - -// ['prod-100' => 'Desk', 'prod-200' => 'Chair'] -``` - -The `pluck` method also supports retrieving nested values using "dot" notation: - -```php -$collection = collect([ - [ - 'name' => 'Laracon', - 'speakers' => [ - 'first_day' => ['Rosa', 'Judith'], - ], - ], - [ - 'name' => 'VueConf', - 'speakers' => [ - 'first_day' => ['Abigail', 'Joey'], - ], - ], -]); - -$plucked = $collection->pluck('speakers.first_day'); - -$plucked->all(); - -// [['Rosa', 'Judith'], ['Abigail', 'Joey']] -``` - -If duplicate keys exist, the last matching element will be inserted into the plucked collection: - -```php -$collection = collect([ - ['brand' => 'Tesla', 'color' => 'red'], - ['brand' => 'Pagani', 'color' => 'white'], - ['brand' => 'Tesla', 'color' => 'black'], - ['brand' => 'Pagani', 'color' => 'orange'], -]); - -$plucked = $collection->pluck('color', 'brand'); - -$plucked->all(); - -// ['Tesla' => 'black', 'Pagani' => 'orange'] -``` - - -#### `pop()` - -The `pop` method removes and returns the last item from the collection: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$collection->pop(); - -// 5 - -$collection->all(); - -// [1, 2, 3, 4] -``` - -You may pass an integer to the `pop` method to remove and return multiple items from the end of a collection: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$collection->pop(3); - -// collect([5, 4, 3]) - -$collection->all(); - -// [1, 2] -``` - - -#### `prepend()` - -The `prepend` method adds an item to the beginning of the collection: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$collection->prepend(0); - -$collection->all(); - -// [0, 1, 2, 3, 4, 5] -``` - -You may also pass a second argument to specify the key of the prepended item: - -```php -$collection = collect(['one' => 1, 'two' => 2]); - -$collection->prepend(0, 'zero'); - -$collection->all(); - -// ['zero' => 0, 'one' => 1, 'two' => 2] -``` - - -#### `pull()` - -The `pull` method removes and returns an item from the collection by its key: - -```php -$collection = collect(['product_id' => 'prod-100', 'name' => 'Desk']); - -$collection->pull('name'); - -// 'Desk' - -$collection->all(); - -// ['product_id' => 'prod-100'] -``` - - -#### `push()` - -The `push` method appends an item to the end of the collection: - -```php -$collection = collect([1, 2, 3, 4]); - -$collection->push(5); - -$collection->all(); - -// [1, 2, 3, 4, 5] -``` - - -#### `put()` - -The `put` method sets the given key and value in the collection: - -```php -$collection = collect(['product_id' => 1, 'name' => 'Desk']); - -$collection->put('price', 100); - -$collection->all(); - -// ['product_id' => 1, 'name' => 'Desk', 'price' => 100] -``` - - -#### `random()` - -The `random` method returns a random item from the collection: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$collection->random(); - -// 4 - (retrieved randomly) -``` - -You may pass an integer to `random` to specify how many items you would like to randomly retrieve. A collection of items is always returned when explicitly passing the number of items you wish to receive: - -```php -$random = $collection->random(3); - -$random->all(); - -// [2, 4, 5] - (retrieved randomly) -``` - -If the collection instance has fewer items than requested, the `random` method will throw an `InvalidArgumentException`. - -The `random` method also accepts a closure, which will receive the current collection instance: - -```php -use Hypervel\Support\Collection; - -$random = $collection->random(fn (Collection $items) => min(10, count($items))); - -$random->all(); - -// [1, 2, 3, 4, 5] - (retrieved randomly) -``` - - -#### `range()` - -The `range` method returns a collection containing integers between the specified range: - -```php -$collection = collect()->range(3, 6); - -$collection->all(); - -// [3, 4, 5, 6] -``` - - -#### `reduce()` - -The `reduce` method reduces the collection to a single value, passing the result of each iteration into the subsequent iteration: - -```php -$collection = collect([1, 2, 3]); - -$total = $collection->reduce(function (?int $carry, int $item) { - return $carry + $item; -}); - -// 6 -``` - -The value for `$carry` on the first iteration is `null`; however, you may specify its initial value by passing a second argument to `reduce`: - -```php -$collection->reduce(function (int $carry, int $item) { - return $carry + $item; -}, 4); - -// 10 -``` - -The `reduce` method also passes array keys in associative collections to the given callback: - -```php -$collection = collect([ - 'usd' => 1400, - 'gbp' => 1200, - 'eur' => 1000, -]); - -$ratio = [ - 'usd' => 1, - 'gbp' => 1.37, - 'eur' => 1.22, -]; - -$collection->reduce(function (int $carry, int $value, int $key) use ($ratio) { - return $carry + ($value * $ratio[$key]); -}); - -// 4264 -``` - - -#### `reduceSpread()` - -The `reduceSpread` method reduces the collection to an array of values, passing the results of each iteration into the subsequent iteration. This method is similar to the `reduce` method; however, it can accept multiple initial values: - -```php -[$creditsRemaining, $batch] = Image::where('status', 'unprocessed') - ->get() - ->reduceSpread(function (int $creditsRemaining, Collection $batch, Image $image) { - if ($creditsRemaining >= $image->creditsRequired()) { - $batch->push($image); - - $creditsRemaining -= $image->creditsRequired(); - } - - return [$creditsRemaining, $batch]; - }, $creditsAvailable, collect()); -``` - - -#### `reject()` - -The `reject` method filters the collection using the given closure. The closure should return `true` if the item should be removed from the resulting collection: - -```php -$collection = collect([1, 2, 3, 4]); - -$filtered = $collection->reject(function (int $value, int $key) { - return $value > 2; -}); - -$filtered->all(); - -// [1, 2] -``` - -For the inverse of the `reject` method, see the [`filter`](#method-filter) method. - - -#### `replace()` - -The `replace` method behaves similarly to `merge`; however, in addition to overwriting matching items that have string keys, the `replace` method will also overwrite items in the collection that have matching numeric keys: - -```php -$collection = collect(['Taylor', 'Abigail', 'James']); - -$replaced = $collection->replace([1 => 'Victoria', 3 => 'Finn']); - -$replaced->all(); - -// ['Taylor', 'Victoria', 'James', 'Finn'] -``` - - -#### `replaceRecursive()` - -This method works like `replace`, but it will recur into arrays and apply the same replacement process to the inner values: - -```php -$collection = collect([ - 'Taylor', - 'Abigail', - [ - 'James', - 'Victoria', - 'Finn' - ] -]); - -$replaced = $collection->replaceRecursive([ - 'Charlie', - 2 => [1 => 'King'] -]); - -$replaced->all(); - -// ['Charlie', 'Abigail', ['James', 'King', 'Finn']] -``` - - -#### `reverse()` - -The `reverse` method reverses the order of the collection's items, preserving the original keys: - -```php -$collection = collect(['a', 'b', 'c', 'd', 'e']); - -$reversed = $collection->reverse(); - -$reversed->all(); - -/* - [ - 4 => 'e', - 3 => 'd', - 2 => 'c', - 1 => 'b', - 0 => 'a', - ] -*/ -``` - - -#### `search()` - -The `search` method searches the collection for the given value and returns its key if found. If the item is not found, `false` is returned: - -```php -$collection = collect([2, 4, 6, 8]); - -$collection->search(4); - -// 1 -``` - -The search is done using a "loose" comparison, meaning a string with an integer value will be considered equal to an integer of the same value. To use "strict" comparison, pass `true` as the second argument to the method: - -```php -collect([2, 4, 6, 8])->search('4', $strict = true); - -// false -``` - -Alternatively, you may provide your own closure to search for the first item that passes a given truth test: - -```php -collect([2, 4, 6, 8])->search(function (int $item, int $key) { - return $item > 5; -}); - -// 2 -``` - - -#### `select()` - -The `select` method selects the given keys from the collection, similar to an SQL `SELECT` statement: - -```php -$users = collect([ - ['name' => 'Taylor Otwell', 'role' => 'Developer', 'status' => 'active'], - ['name' => 'Victoria Faith', 'role' => 'Researcher', 'status' => 'active'], -]); - -$users->select(['name', 'role']); - -/* - [ - ['name' => 'Taylor Otwell', 'role' => 'Developer'], - ['name' => 'Victoria Faith', 'role' => 'Researcher'], - ], -*/ -``` - - -#### `shift()` - -The `shift` method removes and returns the first item from the collection: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$collection->shift(); - -// 1 - -$collection->all(); - -// [2, 3, 4, 5] -``` - -You may pass an integer to the `shift` method to remove and return multiple items from the beginning of a collection: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$collection->shift(3); - -// collect([1, 2, 3]) - -$collection->all(); - -// [4, 5] -``` - - -#### `shuffle()` - -The `shuffle` method randomly shuffles the items in the collection: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$shuffled = $collection->shuffle(); - -$shuffled->all(); - -// [3, 2, 5, 1, 4] - (generated randomly) -``` - - -#### `skip()` - -The `skip` method returns a new collection, with the given number of elements removed from the beginning of the collection: - -```php -$collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - -$collection = $collection->skip(4); - -$collection->all(); - -// [5, 6, 7, 8, 9, 10] -``` - - -#### `skipUntil()` - -The `skipUntil` method skips over items from the collection until the given callback returns `true` and then returns the remaining items in the collection as a new collection instance: - -```php -$collection = collect([1, 2, 3, 4]); - -$subset = $collection->skipUntil(function (int $item) { - return $item >= 3; -}); - -$subset->all(); - -// [3, 4] -``` - -You may also pass a simple value to the `skipUntil` method to skip all items until the given value is found: - -```php -$collection = collect([1, 2, 3, 4]); - -$subset = $collection->skipUntil(3); - -$subset->all(); - -// [3, 4] -``` - -::: warning -If the given value is not found or the callback never returns `true`, the `skipUntil` method will return an empty collection. -::: - - -#### `skipWhile()` - -The `skipWhile` method skips over items from the collection while the given callback returns `true` and then returns the remaining items in the collection as a new collection: - -```php -$collection = collect([1, 2, 3, 4]); - -$subset = $collection->skipWhile(function (int $item) { - return $item <= 3; -}); - -$subset->all(); - -// [4] -``` - -::: warning -If the callback never returns `false`, the `skipWhile` method will return an empty collection. -::: - - -#### `slice()` - -The `slice` method returns a slice of the collection starting at the given index: - -```php -$collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - -$slice = $collection->slice(4); - -$slice->all(); - -// [5, 6, 7, 8, 9, 10] -``` - -If you would like to limit the size of the returned slice, pass the desired size as the second argument to the method: - -```php -$slice = $collection->slice(4, 2); - -$slice->all(); - -// [5, 6] -``` - -The returned slice will preserve keys by default. If you do not wish to preserve the original keys, you can use the [`values`](#method-values) method to reindex them. - - -#### `sliding()` - -The `sliding` method returns a new collection of chunks representing a "sliding window" view of the items in the collection: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$chunks = $collection->sliding(2); - -$chunks->toArray(); - -// [[1, 2], [2, 3], [3, 4], [4, 5]] -``` - -This is especially useful in conjunction with the [`eachSpread`](#method-eachspread) method: - -```php - $transactions->sliding(2)->eachSpread(function (Collection $previous, Collection $current) { - $current->total = $previous->total + $current->amount; - }); - -You may optionally pass a second "step" value, which determines the distance between the first item of every chunk: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$chunks = $collection->sliding(3, step: 2); - -$chunks->toArray(); - -// [[1, 2, 3], [3, 4, 5]] -``` - - -#### `sole()` - -The `sole` method returns the first element in the collection that passes a given truth test, but only if the truth test matches exactly one element: - -```php -collect([1, 2, 3, 4])->sole(function (int $value, int $key) { - return $value === 2; -}); - -// 2 -``` - -You may also pass a key / value pair to the `sole` method, which will return the first element in the collection that matches the given pair, but only if it exactly one element matches: - -```php -$collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 100], -]); - -$collection->sole('product', 'Chair'); - -// ['product' => 'Chair', 'price' => 100] -``` - -Alternatively, you may also call the `sole` method with no argument to get the first element in the collection if there is only one element: - -```php -$collection = collect([ - ['product' => 'Desk', 'price' => 200], -]); - -$collection->sole(); - -// ['product' => 'Desk', 'price' => 200] -``` - -If there are no elements in the collection that should be returned by the `sole` method, an `\Hyperf\Collection\ItemNotFoundException` exception will be thrown. If there is more than one element that should be returned, an `\Hyperf\Collection\MultipleItemsFoundException` will be thrown. - - -#### `some()` - -Alias for the [`contains`](#method-contains) method. - - -#### `sort()` - -The `sort` method sorts the collection. The sorted collection keeps the original array keys, so in the following example we will use the [`values`](#method-values) method to reset the keys to consecutively numbered indexes: - -```php -$collection = collect([5, 3, 1, 2, 4]); - -$sorted = $collection->sort(); - -$sorted->values()->all(); - -// [1, 2, 3, 4, 5] -``` - -If your sorting needs are more advanced, you may pass a callback to `sort` with your own algorithm. Refer to the PHP documentation on [`uasort`](https://secure.php.net/manual/en/function.uasort.php#refsect1-function.uasort-parameters), which is what the collection's `sort` method calls utilizes internally. - -::: note -If you need to sort a collection of nested arrays or objects, see the [`sortBy`](#method-sortby) and [`sortByDesc`](#method-sortbydesc) methods. -::: - - -#### `sortBy()` - -The `sortBy` method sorts the collection by the given key. The sorted collection keeps the original array keys, so in the following example we will use the [`values`](#method-values) method to reset the keys to consecutively numbered indexes: - -```php -$collection = collect([ - ['name' => 'Desk', 'price' => 200], - ['name' => 'Chair', 'price' => 100], - ['name' => 'Bookcase', 'price' => 150], -]); - -$sorted = $collection->sortBy('price'); - -$sorted->values()->all(); - -/* - [ - ['name' => 'Chair', 'price' => 100], - ['name' => 'Bookcase', 'price' => 150], - ['name' => 'Desk', 'price' => 200], - ] -*/ -``` - -The `sortBy` method accepts [sort flags](https://www.php.net/manual/en/function.sort.php) as its second argument: - -```php -$collection = collect([ - ['title' => 'Item 1'], - ['title' => 'Item 12'], - ['title' => 'Item 3'], -]); - -$sorted = $collection->sortBy('title', SORT_NATURAL); - -$sorted->values()->all(); - -/* - [ - ['title' => 'Item 1'], - ['title' => 'Item 3'], - ['title' => 'Item 12'], - ] -*/ -``` - -Alternatively, you may pass your own closure to determine how to sort the collection's values: - -```php -$collection = collect([ - ['name' => 'Desk', 'colors' => ['Black', 'Mahogany']], - ['name' => 'Chair', 'colors' => ['Black']], - ['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']], -]); - -$sorted = $collection->sortBy(function (array $product, int $key) { - return count($product['colors']); -}); - -$sorted->values()->all(); - -/* - [ - ['name' => 'Chair', 'colors' => ['Black']], - ['name' => 'Desk', 'colors' => ['Black', 'Mahogany']], - ['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']], - ] -*/ -``` - -If you would like to sort your collection by multiple attributes, you may pass an array of sort operations to the `sortBy` method. Each sort operation should be an array consisting of the attribute that you wish to sort by and the direction of the desired sort: - -```php -$collection = collect([ - ['name' => 'Taylor Otwell', 'age' => 34], - ['name' => 'Abigail Otwell', 'age' => 30], - ['name' => 'Taylor Otwell', 'age' => 36], - ['name' => 'Abigail Otwell', 'age' => 32], -]); - -$sorted = $collection->sortBy([ - ['name', 'asc'], - ['age', 'desc'], -]); - -$sorted->values()->all(); - -/* - [ - ['name' => 'Abigail Otwell', 'age' => 32], - ['name' => 'Abigail Otwell', 'age' => 30], - ['name' => 'Taylor Otwell', 'age' => 36], - ['name' => 'Taylor Otwell', 'age' => 34], - ] -*/ -``` - -When sorting a collection by multiple attributes, you may also provide closures that define each sort operation: - -```php -$collection = collect([ - ['name' => 'Taylor Otwell', 'age' => 34], - ['name' => 'Abigail Otwell', 'age' => 30], - ['name' => 'Taylor Otwell', 'age' => 36], - ['name' => 'Abigail Otwell', 'age' => 32], -]); - -$sorted = $collection->sortBy([ - fn (array $a, array $b) => $a['name'] <=> $b['name'], - fn (array $a, array $b) => $b['age'] <=> $a['age'], -]); - -$sorted->values()->all(); - -/* - [ - ['name' => 'Abigail Otwell', 'age' => 32], - ['name' => 'Abigail Otwell', 'age' => 30], - ['name' => 'Taylor Otwell', 'age' => 36], - ['name' => 'Taylor Otwell', 'age' => 34], - ] -*/ -``` - - -#### `sortByDesc()` - -This method has the same signature as the [`sortBy`](#method-sortby) method, but will sort the collection in the opposite order. - - -#### `sortDesc()` - -This method will sort the collection in the opposite order as the [`sort`](#method-sort) method: - -```php -$collection = collect([5, 3, 1, 2, 4]); - -$sorted = $collection->sortDesc(); - -$sorted->values()->all(); - -// [5, 4, 3, 2, 1] -``` - -Unlike `sort`, you may not pass a closure to `sortDesc`. Instead, you should use the [`sort`](#method-sort) method and invert your comparison. - - -#### `sortKeys()` - -The `sortKeys` method sorts the collection by the keys of the underlying associative array: - -```php -$collection = collect([ - 'id' => 22345, - 'first' => 'John', - 'last' => 'Doe', -]); - -$sorted = $collection->sortKeys(); - -$sorted->all(); - -/* - [ - 'first' => 'John', - 'id' => 22345, - 'last' => 'Doe', - ] -*/ -``` - - -#### `sortKeysDesc()` - -This method has the same signature as the [`sortKeys`](#method-sortkeys) method, but will sort the collection in the opposite order. - - -#### `sortKeysUsing()` - -The `sortKeysUsing` method sorts the collection by the keys of the underlying associative array using a callback: - -```php -$collection = collect([ - 'ID' => 22345, - 'first' => 'John', - 'last' => 'Doe', -]); - -$sorted = $collection->sortKeysUsing('strnatcasecmp'); - -$sorted->all(); - -/* - [ - 'first' => 'John', - 'ID' => 22345, - 'last' => 'Doe', - ] -*/ -``` - -The callback must be a comparison function that returns an integer less than, equal to, or greater than zero. For more information, refer to the PHP documentation on [`uksort`](https://www.php.net/manual/en/function.uksort.php#refsect1-function.uksort-parameters), which is the PHP function that `sortKeysUsing` method utilizes internally. - - -#### `splice()` - -The `splice` method removes and returns a slice of items starting at the specified index: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$chunk = $collection->splice(2); - -$chunk->all(); - -// [3, 4, 5] - -$collection->all(); - -// [1, 2] -``` - -You may pass a second argument to limit the size of the resulting collection: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$chunk = $collection->splice(2, 1); - -$chunk->all(); - -// [3] - -$collection->all(); - -// [1, 2, 4, 5] -``` - -In addition, you may pass a third argument containing the new items to replace the items removed from the collection: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$chunk = $collection->splice(2, 1, [10, 11]); - -$chunk->all(); - -// [3] - -$collection->all(); - -// [1, 2, 10, 11, 4, 5] -``` - - -#### `split()` - -The `split` method breaks a collection into the given number of groups: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$groups = $collection->split(3); - -$groups->all(); - -// [[1, 2], [3, 4], [5]] -``` - - -#### `splitIn()` - -The `splitIn` method breaks a collection into the given number of groups, filling non-terminal groups completely before allocating the remainder to the final group: - -```php -$collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - -$groups = $collection->splitIn(3); - -$groups->all(); - -// [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10]] -``` - - -#### `sum()` - -The `sum` method returns the sum of all items in the collection: - - collect([1, 2, 3, 4, 5])->sum(); - - // 15 - -If the collection contains nested arrays or objects, you should pass a key that will be used to determine which values to sum: - -```php -$collection = collect([ - ['name' => 'JavaScript: The Good Parts', 'pages' => 176], - ['name' => 'JavaScript: The Definitive Guide', 'pages' => 1096], -]); - -$collection->sum('pages'); - -// 1272 -``` - -In addition, you may pass your own closure to determine which values of the collection to sum: - -```php -$collection = collect([ - ['name' => 'Chair', 'colors' => ['Black']], - ['name' => 'Desk', 'colors' => ['Black', 'Mahogany']], - ['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']], -]); - -$collection->sum(function (array $product) { - return count($product['colors']); -}); - -// 6 -``` - - -#### `take()` - -The `take` method returns a new collection with the specified number of items: - -```php -$collection = collect([0, 1, 2, 3, 4, 5]); - -$chunk = $collection->take(3); - -$chunk->all(); - -// [0, 1, 2] -``` - -You may also pass a negative integer to take the specified number of items from the end of the collection: - -```php -$collection = collect([0, 1, 2, 3, 4, 5]); - -$chunk = $collection->take(-2); - -$chunk->all(); - -// [4, 5] -``` - - -#### `takeUntil()` - -The `takeUntil` method returns items in the collection until the given callback returns `true`: - -```php -$collection = collect([1, 2, 3, 4]); - -$subset = $collection->takeUntil(function (int $item) { - return $item >= 3; -}); - -$subset->all(); - -// [1, 2] -``` - -You may also pass a simple value to the `takeUntil` method to get the items until the given value is found: - -```php -$collection = collect([1, 2, 3, 4]); - -$subset = $collection->takeUntil(3); - -$subset->all(); - -// [1, 2] -``` - -::: warning -If the given value is not found or the callback never returns `true`, the `takeUntil` method will return all items in the collection. -::: - - -#### `takeWhile()` - -The `takeWhile` method returns items in the collection until the given callback returns `false`: - -```php -$collection = collect([1, 2, 3, 4]); - -$subset = $collection->takeWhile(function (int $item) { - return $item < 3; -}); - -$subset->all(); - -// [1, 2] -``` - -::: warning -If the callback never returns `false`, the `takeWhile` method will return all items in the collection. -::: - - -#### `tap()` - -The `tap` method passes the collection to the given callback, allowing you to "tap" into the collection at a specific point and do something with the items while not affecting the collection itself. The collection is then returned by the `tap` method: - -```php -collect([2, 4, 3, 1, 5]) - ->sort() - ->tap(function (Collection $collection) { - Log::debug('Values after sorting', $collection->values()->all()); - }) - ->shift(); - -// 1 -``` - - -#### `times()` - -The static `times` method creates a new collection by invoking the given closure a specified number of times: - -```php -$collection = Collection::times(10, function (int $number) { - return $number * 9; -}); - -$collection->all(); - -// [9, 18, 27, 36, 45, 54, 63, 72, 81, 90] -``` - - -#### `toArray()` - -The `toArray` method converts the collection into a plain PHP `array`. If the collection's values are [Eloquent](/docs/eloquent) models, the models will also be converted to arrays: - -```php -$collection = collect(['name' => 'Desk', 'price' => 200]); - -$collection->toArray(); - -/* - [ - ['name' => 'Desk', 'price' => 200], - ] -*/ -``` - -::: warning -`toArray` also converts all of the collection's nested objects that are an instance of `Arrayable` to an array. If you want to get the raw array underlying the collection, use the [`all`](#method-all) method instead. -::: - - -#### `toJson()` - -The `toJson` method converts the collection into a JSON serialized string: - -```php -$collection = collect(['name' => 'Desk', 'price' => 200]); - -$collection->toJson(); - -// '{"name":"Desk", "price":200}' -``` - - -#### `transform()` - -The `transform` method iterates over the collection and calls the given callback with each item in the collection. The items in the collection will be replaced by the values returned by the callback: - -```php -$collection = collect([1, 2, 3, 4, 5]); - -$collection->transform(function (int $item, int $key) { - return $item * 2; -}); - -$collection->all(); - -// [2, 4, 6, 8, 10] -``` - -::: warning -Unlike most other collection methods, `transform` modifies the collection itself. If you wish to create a new collection instead, use the [`map`](#method-map) method. -::: - - -#### `undot()` - -The `undot` method expands a single-dimensional collection that uses "dot" notation into a multi-dimensional collection: - -```php -$person = collect([ - 'name.first_name' => 'Marie', - 'name.last_name' => 'Valentine', - 'address.line_1' => '2992 Eagle Drive', - 'address.line_2' => '', - 'address.suburb' => 'Detroit', - 'address.state' => 'MI', - 'address.postcode' => '48219' -]); - -$person = $person->undot(); - -$person->toArray(); - -/* - [ - "name" => [ - "first_name" => "Marie", - "last_name" => "Valentine", - ], - "address" => [ - "line_1" => "2992 Eagle Drive", - "line_2" => "", - "suburb" => "Detroit", - "state" => "MI", - "postcode" => "48219", - ], - ] -*/ -``` - - -#### `union()` - -The `union` method adds the given array to the collection. If the given array contains keys that are already in the original collection, the original collection's values will be preferred: - -```php -$collection = collect([1 => ['a'], 2 => ['b']]); - -$union = $collection->union([3 => ['c'], 1 => ['d']]); - -$union->all(); - -// [1 => ['a'], 2 => ['b'], 3 => ['c']] -``` - - -#### `unique()` - -The `unique` method returns all of the unique items in the collection. The returned collection keeps the original array keys, so in the following example we will use the [`values`](#method-values) method to reset the keys to consecutively numbered indexes: - -```php -$collection = collect([1, 1, 2, 2, 3, 4, 2]); - -$unique = $collection->unique(); - -$unique->values()->all(); - -// [1, 2, 3, 4] -``` - -When dealing with nested arrays or objects, you may specify the key used to determine uniqueness: - -```php -$collection = collect([ - ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'], - ['name' => 'iPhone 5', 'brand' => 'Apple', 'type' => 'phone'], - ['name' => 'Apple Watch', 'brand' => 'Apple', 'type' => 'watch'], - ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'], - ['name' => 'Galaxy Gear', 'brand' => 'Samsung', 'type' => 'watch'], -]); - -$unique = $collection->unique('brand'); - -$unique->values()->all(); - -/* - [ - ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'], - ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'], - ] -*/ -``` - -Finally, you may also pass your own closure to the `unique` method to specify which value should determine an item's uniqueness: - -```php -$unique = $collection->unique(function (array $item) { - return $item['brand'].$item['type']; -}); - -$unique->values()->all(); - -/* - [ - ['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'], - ['name' => 'Apple Watch', 'brand' => 'Apple', 'type' => 'watch'], - ['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'], - ['name' => 'Galaxy Gear', 'brand' => 'Samsung', 'type' => 'watch'], - ] -*/ -``` - -The `unique` method uses "loose" comparisons when checking item values, meaning a string with an integer value will be considered equal to an integer of the same value. Use the [`uniqueStrict`](#method-uniquestrict) method to filter using "strict" comparisons. - -::: note -This method's behavior is modified when using [Eloquent Collections](/docs/eloquent-collections#method-unique). -::: - - -#### `uniqueStrict()` - -This method has the same signature as the [`unique`](#method-unique) method; however, all values are compared using "strict" comparisons. - - -#### `unless()` - -The `unless` method will execute the given callback unless the first argument given to the method evaluates to `true`: - -```php -$collection = collect([1, 2, 3]); - -$collection->unless(true, function (Collection $collection) { - return $collection->push(4); -}); - -$collection->unless(false, function (Collection $collection) { - return $collection->push(5); -}); - -$collection->all(); - -// [1, 2, 3, 5] -``` - -A second callback may be passed to the `unless` method. The second callback will be executed when the first argument given to the `unless` method evaluates to `true`: - -```php -$collection = collect([1, 2, 3]); - -$collection->unless(true, function (Collection $collection) { - return $collection->push(4); -}, function (Collection $collection) { - return $collection->push(5); -}); - -$collection->all(); - -// [1, 2, 3, 5] -``` - -For the inverse of `unless`, see the [`when`](#method-when) method. - - -#### `unlessEmpty()` - -Alias for the [`whenNotEmpty`](#method-whennotempty) method. - - -#### `unlessNotEmpty()` - -Alias for the [`whenEmpty`](#method-whenempty) method. - - -#### `unwrap()` - -The static `unwrap` method returns the collection's underlying items from the given value when applicable: - -```php -Collection::unwrap(collect('John Doe')); - -// ['John Doe'] - -Collection::unwrap(['John Doe']); - -// ['John Doe'] - -Collection::unwrap('John Doe'); - -// 'John Doe' -``` - - -#### `value()` - -The `value` method retrieves a given value from the first element of the collection: - -```php -$collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Speaker', 'price' => 400], -]); - -$value = $collection->value('price'); - -// 200 -``` - - -#### `values()` - -The `values` method returns a new collection with the keys reset to consecutive integers: - -```php -$collection = collect([ - 10 => ['product' => 'Desk', 'price' => 200], - 11 => ['product' => 'Desk', 'price' => 200], -]); - -$values = $collection->values(); - -$values->all(); - -/* - [ - 0 => ['product' => 'Desk', 'price' => 200], - 1 => ['product' => 'Desk', 'price' => 200], - ] -*/ -``` - - -#### `when()` - -The `when` method will execute the given callback when the first argument given to the method evaluates to `true`. The collection instance and the first argument given to the `when` method will be provided to the closure: - -```php -$collection = collect([1, 2, 3]); - -$collection->when(true, function (Collection $collection, int $value) { - return $collection->push(4); -}); - -$collection->when(false, function (Collection $collection, int $value) { - return $collection->push(5); -}); - -$collection->all(); - -// [1, 2, 3, 4] -``` - -A second callback may be passed to the `when` method. The second callback will be executed when the first argument given to the `when` method evaluates to `false`: - -```php -$collection = collect([1, 2, 3]); - -$collection->when(false, function (Collection $collection, int $value) { - return $collection->push(4); -}, function (Collection $collection) { - return $collection->push(5); -}); - -$collection->all(); - -// [1, 2, 3, 5] -``` - -For the inverse of `when`, see the [`unless`](#method-unless) method. - - -#### `whenEmpty()` - -The `whenEmpty` method will execute the given callback when the collection is empty: - -```php -$collection = collect(['Michael', 'Tom']); - -$collection->whenEmpty(function (Collection $collection) { - return $collection->push('Adam'); -}); - -$collection->all(); - -// ['Michael', 'Tom'] - - -$collection = collect(); - -$collection->whenEmpty(function (Collection $collection) { - return $collection->push('Adam'); -}); - -$collection->all(); - -// ['Adam'] -``` - -A second closure may be passed to the `whenEmpty` method that will be executed when the collection is not empty: - -```php -$collection = collect(['Michael', 'Tom']); - -$collection->whenEmpty(function (Collection $collection) { - return $collection->push('Adam'); -}, function (Collection $collection) { - return $collection->push('Taylor'); -}); - -$collection->all(); - -// ['Michael', 'Tom', 'Taylor'] -``` - -For the inverse of `whenEmpty`, see the [`whenNotEmpty`](#method-whennotempty) method. - - -#### `whenNotEmpty()` - -The `whenNotEmpty` method will execute the given callback when the collection is not empty: - -```php -$collection = collect(['michael', 'tom']); - -$collection->whenNotEmpty(function (Collection $collection) { - return $collection->push('adam'); -}); - -$collection->all(); - -// ['michael', 'tom', 'adam'] - - -$collection = collect(); - -$collection->whenNotEmpty(function (Collection $collection) { - return $collection->push('adam'); -}); - -$collection->all(); - -// [] -``` - -A second closure may be passed to the `whenNotEmpty` method that will be executed when the collection is empty: - -```php -$collection = collect(); - -$collection->whenNotEmpty(function (Collection $collection) { - return $collection->push('adam'); -}, function (Collection $collection) { - return $collection->push('taylor'); -}); - -$collection->all(); - -// ['taylor'] -``` - -For the inverse of `whenNotEmpty`, see the [`whenEmpty`](#method-whenempty) method. - - -#### `where()` - -The `where` method filters the collection by a given key / value pair: - -```php -$collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 100], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Door', 'price' => 100], -]); - -$filtered = $collection->where('price', 100); - -$filtered->all(); - -/* - [ - ['product' => 'Chair', 'price' => 100], - ['product' => 'Door', 'price' => 100], - ] -*/ -``` - -The `where` method uses "loose" comparisons when checking item values, meaning a string with an integer value will be considered equal to an integer of the same value. Use the [`whereStrict`](#method-wherestrict) method to filter using "strict" comparisons. - -Optionally, you may pass a comparison operator as the second parameter. Supported operators are: '===', '!==', '!=', '==', '=', '<>', '>', '<', '>=', and '<=': - -```php -$collection = collect([ - ['name' => 'Jim', 'deleted_at' => '2019-01-01 00:00:00'], - ['name' => 'Sally', 'deleted_at' => '2019-01-02 00:00:00'], - ['name' => 'Sue', 'deleted_at' => null], -]); - -$filtered = $collection->where('deleted_at', '!=', null); - -$filtered->all(); - -/* - [ - ['name' => 'Jim', 'deleted_at' => '2019-01-01 00:00:00'], - ['name' => 'Sally', 'deleted_at' => '2019-01-02 00:00:00'], - ] -*/ -``` - - -#### `whereStrict()` - -This method has the same signature as the [`where`](#method-where) method; however, all values are compared using "strict" comparisons. - - -#### `whereBetween()` - -The `whereBetween` method filters the collection by determining if a specified item value is within a given range: - -```php -$collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 80], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Pencil', 'price' => 30], - ['product' => 'Door', 'price' => 100], -]); - -$filtered = $collection->whereBetween('price', [100, 200]); - -$filtered->all(); - -/* - [ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Door', 'price' => 100], - ] -*/ -``` - - -#### `whereIn()` - -The `whereIn` method removes elements from the collection that do not have a specified item value that is contained within the given array: - -```php -$collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 100], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Door', 'price' => 100], -]); - -$filtered = $collection->whereIn('price', [150, 200]); - -$filtered->all(); - -/* - [ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Bookcase', 'price' => 150], - ] -*/ -``` - -The `whereIn` method uses "loose" comparisons when checking item values, meaning a string with an integer value will be considered equal to an integer of the same value. Use the [`whereInStrict`](#method-whereinstrict) method to filter using "strict" comparisons. - - -#### `whereInStrict()` - -This method has the same signature as the [`whereIn`](#method-wherein) method; however, all values are compared using "strict" comparisons. - - -#### `whereInstanceOf()` - -The `whereInstanceOf` method filters the collection by a given class type: - -```php -use App\Models\User; -use App\Models\Post; - -$collection = collect([ - new User, - new User, - new Post, -]); - -$filtered = $collection->whereInstanceOf(User::class); - -$filtered->all(); - -// [App\Models\User, App\Models\User] -``` - - -#### `whereNotBetween()` - -The `whereNotBetween` method filters the collection by determining if a specified item value is outside of a given range: - -```php -$collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 80], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Pencil', 'price' => 30], - ['product' => 'Door', 'price' => 100], -]); - -$filtered = $collection->whereNotBetween('price', [100, 200]); - -$filtered->all(); - -/* - [ - ['product' => 'Chair', 'price' => 80], - ['product' => 'Pencil', 'price' => 30], - ] -*/ -``` - - -#### `whereNotIn()` - -The `whereNotIn` method removes elements from the collection that have a specified item value that is contained within the given array: - -```php -$collection = collect([ - ['product' => 'Desk', 'price' => 200], - ['product' => 'Chair', 'price' => 100], - ['product' => 'Bookcase', 'price' => 150], - ['product' => 'Door', 'price' => 100], -]); - -$filtered = $collection->whereNotIn('price', [150, 200]); - -$filtered->all(); - -/* - [ - ['product' => 'Chair', 'price' => 100], - ['product' => 'Door', 'price' => 100], - ] -*/ -``` - -The `whereNotIn` method uses "loose" comparisons when checking item values, meaning a string with an integer value will be considered equal to an integer of the same value. Use the [`whereNotInStrict`](#method-wherenotinstrict) method to filter using "strict" comparisons. - - -#### `whereNotInStrict()` - -This method has the same signature as the [`whereNotIn`](#method-wherenotin) method; however, all values are compared using "strict" comparisons. - - -#### `whereNotNull()` - -The `whereNotNull` method returns items from the collection where the given key is not `null`: - -```php -$collection = collect([ - ['name' => 'Desk'], - ['name' => null], - ['name' => 'Bookcase'], -]); - -$filtered = $collection->whereNotNull('name'); - -$filtered->all(); - -/* - [ - ['name' => 'Desk'], - ['name' => 'Bookcase'], - ] -*/ -``` - - -#### `whereNull()` - -The `whereNull` method returns items from the collection where the given key is `null`: - -```php -$collection = collect([ - ['name' => 'Desk'], - ['name' => null], - ['name' => 'Bookcase'], -]); - -$filtered = $collection->whereNull('name'); - -$filtered->all(); - -/* - [ - ['name' => null], - ] -*/ -``` - - -#### `wrap()` - -The static `wrap` method wraps the given value in a collection when applicable: - -```php -use Hypervel\Support\Collection; - -$collection = Collection::wrap('John Doe'); - -$collection->all(); - -// ['John Doe'] - -$collection = Collection::wrap(['John Doe']); - -$collection->all(); - -// ['John Doe'] - -$collection = Collection::wrap(collect('John Doe')); - -$collection->all(); - -// ['John Doe'] -``` - - -#### `zip()` - -The `zip` method merges together the values of the given array with the values of the original collection at their corresponding index: - -```php -$collection = collect(['Chair', 'Desk']); - -$zipped = $collection->zip([100, 200]); - -$zipped->all(); - -// [['Chair', 100], ['Desk', 200]] -``` - - -## Higher Order Messages - -Collections also provide support for "higher order messages", which are short-cuts for performing common actions on collections. The collection methods that provide higher order messages are: [`average`](#method-average), [`avg`](#method-avg), [`contains`](#method-contains), [`each`](#method-each), [`every`](#method-every), [`filter`](#method-filter), [`first`](#method-first), [`flatMap`](#method-flatmap), [`groupBy`](#method-groupby), [`keyBy`](#method-keyby), [`map`](#method-map), [`max`](#method-max), [`min`](#method-min), [`partition`](#method-partition), [`reject`](#method-reject), [`skipUntil`](#method-skipuntil), [`skipWhile`](#method-skipwhile), [`some`](#method-some), [`sortBy`](#method-sortby), [`sortByDesc`](#method-sortbydesc), [`sum`](#method-sum), [`takeUntil`](#method-takeuntil), [`takeWhile`](#method-takewhile), and [`unique`](#method-unique). - -Each higher order message can be accessed as a dynamic property on a collection instance. For instance, let's use the `each` higher order message to call a method on each object within a collection: - -```php -use App\Models\User; - -$users = User::where('votes', '>', 500)->get(); - -$users->each->markAsVip(); -``` - -Likewise, we can use the `sum` higher order message to gather the total number of "votes" for a collection of users: - -```php -$users = User::where('group', 'Development')->get(); - -return $users->sum->votes; -``` - - -## Lazy Collections - - -### Introduction - -::: warning -Before learning more about Hypervel's lazy collections, take some time to familiarize yourself with [PHP generators](https://www.php.net/manual/en/language.generators.overview.php). -::: - -To supplement the already powerful `Collection` class, the `LazyCollection` class leverages PHP's [generators](https://www.php.net/manual/en/language.generators.overview.php) to allow you to work with very large datasets while keeping memory usage low. - -For example, imagine your application needs to process a multi-gigabyte log file while taking advantage of Hypervel's collection methods to parse the logs. Instead of reading the entire file into memory at once, lazy collections may be used to keep only a small part of the file in memory at a given time: - -```php -use App\Models\LogEntry; -use Hyperf\Collection\LazyCollection; - -LazyCollection::make(function () { - $handle = fopen('log.txt', 'r'); - - while (($line = fgets($handle)) !== false) { - yield $line; - } -})->chunk(4)->map(function (array $lines) { - return LogEntry::fromLines($lines); -})->each(function (LogEntry $logEntry) { - // Process the log entry... -}); -``` - -Or, imagine you need to iterate through 10,000 Eloquent models. When using traditional Hypervel collections, all 10,000 Eloquent models must be loaded into memory at the same time: - -```php -use App\Models\User; - -$users = User::all()->filter(function (User $user) { - return $user->id > 500; -}); -``` - -However, the query builder's `cursor` method returns a `LazyCollection` instance. This allows you to still only run a single query against the database but also only keep one Eloquent model loaded in memory at a time. In this example, the `filter` callback is not executed until we actually iterate over each user individually, allowing for a drastic reduction in memory usage: - -```php -use App\Models\User; - -$users = User::cursor()->filter(function (User $user) { - return $user->id > 500; -}); - -foreach ($users as $user) { - echo $user->id; -} -``` - - -### Creating Lazy Collections - -To create a lazy collection instance, you should pass a PHP generator function to the collection's `make` method: - -```php -use Hyperf\Collection\LazyCollection; - -LazyCollection::make(function () { - $handle = fopen('log.txt', 'r'); - - while (($line = fgets($handle)) !== false) { - yield $line; - } -}); -``` - - -### The Enumerable Contract - -Almost all methods available on the `Collection` class are also available on the `LazyCollection` class. Both of these classes implement the `Hyperf\Collection\Enumerable` contract, which defines the following methods: - - - -
    - -[all](#method-all) -[average](#method-average) -[avg](#method-avg) -[chunk](#method-chunk) -[chunkWhile](#method-chunkwhile) -[collapse](#method-collapse) -[collect](#method-collect) -[combine](#method-combine) -[concat](#method-concat) -[contains](#method-contains) -[containsStrict](#method-containsstrict) -[count](#method-count) -[countBy](#method-countBy) -[crossJoin](#method-crossjoin) -[dd](#method-dd) -[diff](#method-diff) -[diffAssoc](#method-diffassoc) -[diffKeys](#method-diffkeys) -[dump](#method-dump) -[duplicates](#method-duplicates) -[duplicatesStrict](#method-duplicatesstrict) -[each](#method-each) -[eachSpread](#method-eachspread) -[every](#method-every) -[except](#method-except) -[filter](#method-filter) -[first](#method-first) -[firstOrFail](#method-first-or-fail) -[firstWhere](#method-first-where) -[flatMap](#method-flatmap) -[flatten](#method-flatten) -[flip](#method-flip) -[forPage](#method-forpage) -[get](#method-get) -[groupBy](#method-groupby) -[has](#method-has) -[implode](#method-implode) -[intersect](#method-intersect) -[intersectAssoc](#method-intersectAssoc) -[intersectByKeys](#method-intersectbykeys) -[isEmpty](#method-isempty) -[isNotEmpty](#method-isnotempty) -[join](#method-join) -[keyBy](#method-keyby) -[keys](#method-keys) -[last](#method-last) -[macro](#method-macro) -[make](#method-make) -[map](#method-map) -[mapInto](#method-mapinto) -[mapSpread](#method-mapspread) -[mapToGroups](#method-maptogroups) -[mapWithKeys](#method-mapwithkeys) -[max](#method-max) -[median](#method-median) -[merge](#method-merge) -[mergeRecursive](#method-mergerecursive) -[min](#method-min) -[mode](#method-mode) -[nth](#method-nth) -[only](#method-only) -[pad](#method-pad) -[partition](#method-partition) -[pipe](#method-pipe) -[pluck](#method-pluck) -[random](#method-random) -[reduce](#method-reduce) -[reject](#method-reject) -[replace](#method-replace) -[replaceRecursive](#method-replacerecursive) -[reverse](#method-reverse) -[search](#method-search) -[shuffle](#method-shuffle) -[skip](#method-skip) -[slice](#method-slice) -[sole](#method-sole) -[some](#method-some) -[sort](#method-sort) -[sortBy](#method-sortby) -[sortByDesc](#method-sortbydesc) -[sortKeys](#method-sortkeys) -[sortKeysDesc](#method-sortkeysdesc) -[split](#method-split) -[sum](#method-sum) -[take](#method-take) -[tap](#method-tap) -[times](#method-times) -[toArray](#method-toarray) -[toJson](#method-tojson) -[union](#method-union) -[unique](#method-unique) -[uniqueStrict](#method-uniquestrict) -[unless](#method-unless) -[unlessEmpty](#method-unlessempty) -[unlessNotEmpty](#method-unlessnotempty) -[unwrap](#method-unwrap) -[values](#method-values) -[when](#method-when) -[whenEmpty](#method-whenempty) -[whenNotEmpty](#method-whennotempty) -[where](#method-where) -[whereStrict](#method-wherestrict) -[whereBetween](#method-wherebetween) -[whereIn](#method-wherein) -[whereInStrict](#method-whereinstrict) -[whereInstanceOf](#method-whereinstanceof) -[whereNotBetween](#method-wherenotbetween) -[whereNotIn](#method-wherenotin) -[whereNotInStrict](#method-wherenotinstrict) -[wrap](#method-wrap) -[zip](#method-zip) - -
    - -::: warning -Methods that mutate the collection (such as `shift`, `pop`, `prepend` etc.) are **not** available on the `LazyCollection` class. -::: - - -### Lazy Collection Methods - -In addition to the methods defined in the `Enumerable` contract, the `LazyCollection` class contains the following methods: - - -#### `takeUntilTimeout()` - -The `takeUntilTimeout` method returns a new lazy collection that will enumerate values until the specified time. After that time, the collection will then stop enumerating: - -```php -$lazyCollection = LazyCollection::times(INF) - ->takeUntilTimeout(now()->addMinute()); - -$lazyCollection->each(function (int $number) { - dump($number); - - sleep(1); -}); - -// 1 -// 2 -// ... -// 58 -// 59 -``` - -To illustrate the usage of this method, imagine an application that submits invoices from the database using a cursor. You could define a [scheduled task](/docs/scheduling) that runs every 15 minutes and only processes invoices for a maximum of 14 minutes: - -```php -use App\Models\Invoice; -use Carbon\Carbon; - -Invoice::pending()->cursor() - ->takeUntilTimeout( - Carbon::createFromTimestamp(LARAVEL_HYPERF_START)->add(14, 'minutes') - ) - ->each(fn (Invoice $invoice) => $invoice->submit()); -``` - - -#### `tapEach()` - -While the `each` method calls the given callback for each item in the collection right away, the `tapEach` method only calls the given callback as the items are being pulled out of the list one by one: - -```php -// Nothing has been dumped so far... -$lazyCollection = LazyCollection::times(INF)->tapEach(function (int $value) { - dump($value); -}); - -// Three items are dumped... -$array = $lazyCollection->take(3)->all(); - -// 1 -// 2 -// 3 -``` - - -#### `remember()` - -The `remember` method returns a new lazy collection that will remember any values that have already been enumerated and will not retrieve them again on subsequent collection enumerations: - -```php -// No query has been executed yet... -$users = User::cursor()->remember(); - -// The query is executed... -// The first 5 users are hydrated from the database... -$users->take(5)->all(); - -// First 5 users come from the collection's cache... -// The rest are hydrated from the database... -$users->take(20)->all(); -``` diff --git a/_archive/docs/0.3/docs/configuration.md b/_archive/docs/0.3/docs/configuration.md deleted file mode 100644 index 9f14d7d8bf..0000000000 --- a/_archive/docs/0.3/docs/configuration.md +++ /dev/null @@ -1,178 +0,0 @@ -# Configuration -[[toc]] - -## Introduction - -All of the configuration files for the Hypervel framework are stored in the `config` directory. Each option is documented, so feel free to look through the files and get familiar with the options available to you. - -These configuration files allow you to configure things like your database connection information, your mail server information, as well as various other core configuration values such as your application URL and encryption key. - -### The `about` Command - -Hypervel can display an overview of your application's configuration, drivers, and environment via the `about` Artisan command. - -```shell:no-line-numbers -php artisan about -``` - -If you're only interested in a particular section of the application overview output, you may filter for that section using the `--only` option: - -```shell:no-line-numbers -php artisan about --only=environment -``` - -Or, to explore a specific configuration file's values in detail, you may use the `config:show` Artisan command: - -```shell:no-line-numbers -php artisan config:show database -``` - -## Unsupported Recursive Config Resolution - -Since Hypervel initializes the Configuration service at a very early stage in the framework's underlying mechanism, which differs from Laravel, the related configs are loaded into the framework at an extremely early phase. - -At this point, **Config Instance** is not ready while reading config files. This means you shouldn't refer to other config values in the config files. - -For example, you will get errors with the following config declaration: - -```php -return [ - 'foo' => config('bar.title'), -]; -``` - -::: warning -Some special config files will be loaded by Hypervel before the app container is ready, such as `config/app.php`, `config/dependencies.php`, `config/annotations.php`. You shouldn't call any functions that will require app container in these config files. -::: - -## Environment Configuration - -It is often helpful to have different configuration values based on the environment where the application is running. For example, you may wish to use a different cache driver locally than you do on your production server. - -To make this a cinch, Hypervel utilizes the [DotEnv](https://github.com/vlucas/phpdotenv) PHP library. In a fresh Hypervel installation, the root directory of your application will contain a `.env.example` file that defines many common environment variables. During the Hypervel installation process, this file will automatically be copied to `.env`. - -Hypervel's default `.env` file contains some common configuration values that may differ based on whether your application is running locally or on a production web server. These values are then read by the configuration files within the `config` directory using Hypervel's `env` function. - -If you are developing with a team, you may wish to continue including and updating the `.env.example` file with your application. By putting placeholder values in the example configuration file, other developers on your team can clearly see which environment variables are needed to run your application. - -::: note -Any variable in your `.env` file can be overridden by external environment variables such as server-level or system-level environment variables. -::: - -#### Environment File Security - -Your `.env` file should not be committed to your application's source control, since each developer / server using your application could require a different environment configuration. Furthermore, this would be a security risk in the event an intruder gains access to your source control repository, since any sensitive credentials would get exposed. - -### Environment Variable Types - -All variables in your `.env` files are typically parsed as strings, so some reserved values have been created to allow you to return a wider range of types from the `env()` function: - -
    - -| `.env` Value | `env()` Value | -| ------------ | ------------- | -| true | (bool) true | -| (true) | (bool) true | -| false | (bool) false | -| (false) | (bool) false | -| empty | (string) '' | -| (empty) | (string) '' | -| null | (null) null | -| (null) | (null) null | - -
    - -If you need to define an environment variable with a value that contains spaces, you may do so by enclosing the value in double quotes: - -```ini -APP_NAME="My Application" -``` - -### Retrieving Environment Configuration - -You can use the `env` function to retrieve values from these variables in your configuration files. In fact, if you review the Hypervel configuration files, you will notice many of the options are already using this function: - -```php -'debug' => env('APP_DEBUG', false), -``` - -The second value passed to the `env` function is the "default value". This value will be returned if no environment variable exists for the given key. - -### Determining the Current Environment - -The current application environment is determined via the `APP_ENV` variable from your `.env` file. You may access this value via the `environment` method on the `App` [facade](/docs/facades): - -```php -use Hypervel\Support\Facades\App; - -$environment = App::environment(); -``` - -You may also pass arguments to the `environment` method to determine if the environment matches a given value. The method will return `true` if the environment matches any of the given values: - -```php -if (App::environment('local')) { - // The environment is local -} - -if (App::environment(['local', 'staging'])) { - // The environment is either local OR staging... -} -``` - -::: note -The current application environment detection can be overridden by defining a server-level `APP_ENV` environment variable. -::: - -## Accessing Configuration Values - -You may easily access your configuration values using the `Config` facade or global `config` function from anywhere in your application. The configuration values may be accessed using "dot" syntax, which includes the name of the file and option you wish to access. A default value may also be specified and will be returned if the configuration option does not exist: - -```php -use Hypervel\Support\Facades\Config; - -$value = Config::get('app.timezone'); - -$value = config('app.timezone'); - -// Retrieve a default value if the configuration value does not exist... -$value = config('app.timezone', 'Asia/Seoul'); -``` - -To set configuration values at runtime, you may invoke the `Config` facade's `set` method or pass an array to the `config` function: - -```php -Config::set('app.timezone', 'America/Chicago'); - -config(['app.timezone' => 'America/Chicago']); -``` - -To assist with static analysis, the `Config` facade also provides typed configuration retrieval methods. If the retrieved configuration value does not match the expected type, an exception will be thrown: - -```php -Config::string('config-key'); -Config::integer('config-key'); -Config::float('config-key'); -Config::boolean('config-key'); -Config::array('config-key'); -``` - -## Configuration Publishing - -Most of Hypervel's configuration files are already published in your application's `config` directory; however, certain configuration files like `cors.php` and `view.php` are not published by default, as most applications will never need to modify them. - -However, you may use the `vendor:publish` Artisan command to publish packages' assets including configuration files that are not published by default: - -```shell:no-line-numbers -php artisan vendor:publish package - -php artisan vendor:publish --all -``` - -## Debug Mode - -The `debug` option in your `config/app.php` configuration file determines how much information about an error is actually displayed to the user. By default, this option is set to respect the value of the `APP_DEBUG` environment variable, which is stored in your `.env` file. - -::: warning -For local development, you should set the `APP_DEBUG` environment variable to `true`. **In your production environment, this value should always be `false`. If the variable is set to `true` in production, you risk exposing sensitive configuration values to your application's end users.** -::: diff --git a/_archive/docs/0.3/docs/console-tests.md b/_archive/docs/0.3/docs/console-tests.md deleted file mode 100644 index 48bf7bd1c8..0000000000 --- a/_archive/docs/0.3/docs/console-tests.md +++ /dev/null @@ -1,140 +0,0 @@ -# Console Tests -[[toc]] - -## Introduction - -In addition to simplifying HTTP testing, Hypervel provides a simple API for testing your application's [custom console commands](/docs/artisan). - -## Success / Failure Expectations - -To get started, let's explore how to make assertions regarding an Artisan command's exit code. To accomplish this, we will use the `artisan` method to invoke an Artisan command from our test. Then, we will use the `assertExitCode` method to assert that the command completed with a given exit code: - -```php -/** - * Test a console command. - */ -public function testConsoleCommand(): void -{ - $this->artisan('inspire')->assertExitCode(0); -} -``` - -You may use the `assertNotExitCode` method to assert that the command did not exit with a given exit code: - -```php -$this->artisan('inspire')->assertNotExitCode(1); -``` - -Of course, all terminal commands typically exit with a status code of `0` when they are successful and a non-zero exit code when they are not successful. Therefore, for convenience, you may utilize the `assertSuccessful` and `assertFailed` assertions to assert that a given command exited with a successful exit code or not: - -```php -$this->artisan('inspire')->assertSuccessful(); - -$this->artisan('inspire')->assertFailed(); -``` - -## Input / Output Expectations - -Hypervel allows you to easily "mock" user input for your console commands using the `expectsQuestion` method. In addition, you may specify the exit code and text that you expect to be output by the console command using the `assertExitCode` and `expectsOutput` methods. For example, consider the following console command: - -```php -Artisan::command('question', function () { - $name = $this->ask('What is your name?'); - - $language = $this->choice('Which language do you prefer?', [ - 'PHP', - 'Ruby', - 'Python', - ]); - - $this->line('Your name is '.$name.' and you prefer '.$language.'.'); -}); -``` - -You may test this command with the following test: - -```php -/** - * Test a console command. - */ -public function testConsoleCommand(): void -{ - $this->artisan('question') - ->expectsQuestion('What is your name?', 'Taylor Otwell') - ->expectsQuestion('Which language do you prefer?', 'PHP') - ->expectsOutput('Your name is Taylor Otwell and you prefer PHP.') - ->doesntExpectOutput('Your name is Taylor Otwell and you prefer Ruby.') - ->assertExitCode(0); -} -``` - -If you are utilizing the `search` or `multisearch` functions provided by [Hypervel Prompts](/docs/prompts), you may use the `expectsSearch` assertion to mock the user's input, search results, and selection: - -```php -/** - * Test a console command. - */ -public function test_console_command(): void -{ - $this->artisan('example') - ->expectsSearch('What is your name?', search: 'Tay', answers: [ - 'Taylor Otwell', - 'Taylor Swift', - 'Darian Taylor' - ], answer: 'Taylor Otwell') - ->assertExitCode(0); -} -``` - -You may also assert that a console command does not generate any output using the `doesntExpectOutput` method: - -```php -/** - * Test a console command. - */ -public function testConsoleCommand(): void -{ - $this->artisan('example') - ->doesntExpectOutput() - ->assertExitCode(0); -} -``` - -The `expectsOutputToContain` and `doesntExpectOutputToContain` methods may be used to make assertions against a portion of the output: - -```php -/** - * Test a console command. - */ -public function testConsoleCommand(): void -{ - $this->artisan('example') - ->expectsOutputToContain('Taylor') - ->assertExitCode(0); -} -``` - -#### Confirmation Expectations - -When writing a command which expects confirmation in the form of a "yes" or "no" answer, you may utilize the `expectsConfirmation` method: - -```php -$this->artisan('module:import') - ->expectsConfirmation('Do you really wish to run this command?', 'no') - ->assertExitCode(1); -``` - -#### Table Expectations - -If your command displays a table of information using Artisan's `table` method, it can be cumbersome to write output expectations for the entire table. Instead, you may use the `expectsTable` method. This method accepts the table's headers as its first argument and the table's data as its second argument: - -```php -$this->artisan('users:all') - ->expectsTable([ - 'ID', - 'Email', - ], [ - [1, 'taylor@example.com'], - [2, 'abigail@example.com'], - ]); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/container.md b/_archive/docs/0.3/docs/container.md deleted file mode 100644 index 945114f4a2..0000000000 --- a/_archive/docs/0.3/docs/container.md +++ /dev/null @@ -1,382 +0,0 @@ -# Service Container -[[toc]] - -## Introduction - -Service container in Hyperf follows [PSR-11](https://www.php-fig.org/psr/psr-11/) and provides minimum abilities for managing service bindings. And it's designed for long-life applications and with AOP support. - -Based on Hyperf's container, Hypervel extends a bunch of features from Laravel's container such as alias and container events. - -The Hypervel service container is a powerful tool for managing class dependencies and performing dependency injection. Dependency injection is a fancy phrase that essentially means this: class dependencies are "injected" into the class via the constructor or, in some cases, "setter" methods. - -Let's look at a simple example: - -```php - $this->apple->findPodcast($id) - ]); - } -} -``` - -In this example, the `PodcastController` needs to retrieve podcasts from a data source such as Apple Music. So, we will **inject** a service that is able to retrieve podcasts. Since the service is injected, we are able to easily "mock", or create a dummy implementation of the `AppleMusic` service when testing our application. - -A deep understanding of the Hypervel service container is essential to building a powerful, large application, as well as for contributing to the Laravel core itself. - -### Differences between Laravel Container - -To ensure compatibility with the Hyperf Container, there are some important differences you need to be aware of: - -* Different from Laravel, every binding in the Hyperf Container is a `singleton`. You need to ensure that every instance can be shared across different requests and coroutines. **Do not store states in the objects!** Instead, use [Context](/docs/context) if you need to maintain states within objects. - -* If you need to get a new instance rather than fetching the same singleton object from the container, you can use the `make` function. This will initiate a new object and inject related dependencies (note that these dependencies are still singletons). - -* The `has` function in the Hyperf Container follows PSR-11 standards. It only determines if the target can be resolved. It will return true if the container can return an entry for the given identifier. `has($id)` returning true does not mean that `get($id)` will not throw an exception. It does, however, mean that `get($id)` will not throw a `NotFoundExceptionInterface`. You can use the `bound` method as an alternative for checking if an ID is bound to the container. - -::: important -Please read the above description carefully before using the container in Hypervel! -::: - -### Zero Configuration Resolution - -If a class has no dependencies or only depends on other concrete classes (not interfaces), the container does not need to be instructed on how to resolve that class. For example, you may place the following code in your `routes/web.php` file: - -```php -app` property. We can register a binding using the `bind` method, passing the class or interface name that we wish to register along with a closure that returns an instance of the class: - -```php -use App\Services\Transistor; -use App\Services\PodcastParser; -use Hypervel\Foundation\Contracts\Application; - -$this->app->bind(Transistor::class, function (Application $app) { - return new Transistor($app->get(PodcastParser::class)); -}); -``` - -Note that we receive the container itself as an argument to the resolver. We can then use the container to resolve sub-dependencies of the object we are building. - -As mentioned, you will typically be interacting with the container within service providers; however, if you would like to interact with the container outside of a service provider, you may do so via the `App` [facade](/docs/facades): - -```php -use App\Services\Transistor; -use Hypervel\Foundation\Contracts\Application; -use Hypervel\Support\Facades\App; - -App::bind(Transistor::class, function (Application $app) { - // ... -}); -``` - -You may use the `bindIf` method to register a container binding only if a binding has not already been registered for the given type: - -```php -$this->app->bindIf(Transistor::class, function (Application $app) { - return new Transistor($app->get(PodcastParser::class)); -}); -``` - -::: note -There is no need to bind classes into the container if they do not depend on any interfaces. The container does not need to be instructed on how to build these objects, since it can automatically resolve these objects using reflection. -::: - -#### Binding Instances - -You may also bind an existing object instance into the container using the `instance` method. The given instance will always be returned on subsequent calls into the container: - -```php -use App\Services\Transistor; -use App\Services\PodcastParser; - -$service = new Transistor(new PodcastParser); - -$this->app->instance(Transistor::class, $service); -``` - -### Binding Interfaces to Implementations - -A very powerful feature of the service container is its ability to bind an interface to a given implementation. For example, let's assume we have an `EventPusher` interface and a `RedisEventPusher` implementation. Once we have coded our `RedisEventPusher` implementation of this interface, we can register it with the service container like so: - -```php -use App\Contracts\EventPusher; -use App\Services\RedisEventPusher; - -$this->app->bind(EventPusher::class, RedisEventPusher::class); -``` - -This statement tells the container that it should inject the `RedisEventPusher` when a class needs an implementation of `EventPusher`. Now we can type-hint the `EventPusher` interface in the constructor of a class that is resolved by the container. Remember, controllers, event listeners, middleware, and various other types of classes within Laravel Hyper applications are always resolved using the container: - -```php -use App\Contracts\EventPusher; - -/** - * Create a new class instance. - */ -public function __construct( - protected EventPusher $pusher -) {} -``` - -### Extending Bindings - -The `extend` method allows the modification of resolved services. For example, when a service is resolved, you may run additional code to decorate or configure the service. The `extend` method accepts two arguments, the service class you're extending and a closure that should return the modified service. The closure receives the service being resolved and the container instance: - -```php -$this->app->extend(Service::class, function (Service $service, Application $app) { - return new DecoratedService($service); -}); -``` - -## Resolving - -### The `get` Method - -You may use the `get` method to resolve a class instance from the container. The `get` method accepts the name of the class or interface you wish to resolve: - -```php -use App\Services\Transistor; - -$transistor = $this->app->get(Transistor::class); -``` - -::: important -Every bindings fetched via `get` function is singleton. -::: - -### The `make` Method - -If you want to get new instance object from the container, you may use the `make` method to resolve a class instance from the container. It's suitable for short-lived use cases. - -```php -use App\Services\Transistor; - -$transistor = $this->app->make(Transistor::class); -``` - -If some of your class's dependencies are not resolvable via the container, you may inject them by passing them as an associative array into the `makeWith` method. For example, we may manually pass the `$id` constructor argument required by the `Transistor` service: - -```php -use App\Services\Transistor; - -$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]); -``` - -The `bound` method may be used to determine if a class or interface has been explicitly bound in the container: - -```php -if ($this->app->bound(Transistor::class)) { - // ... -} -``` - -If you are outside of a service provider in a location of your code that does not have access to the `$app` variable, you may use the `App` [facade](/docs/facades) or the `app` [helper](/docs/helpers#method-app) to resolve a class instance from the container: - -```php -use App\Services\Transistor; -use Hypervel\Support\Facades\App; - -$transistor = App::make(Transistor::class); - -$transistor = app(Transistor::class); -``` - -If you would like to have the Hypervel container instance itself injected into a class that is being resolved by the container, you may type-hint the `Hypervel\Container\Contracts\Container` class on your class's constructor: - -```php -use Hypervel\Container\Contracts\Container; - -/** - * Create a new class instance. - */ -public function __construct( - protected Container $container -) {} -``` - -::: tip -You can get the container instance by this bindings: - -* `app` -* `Psr\Container\ContainerInterface::class` -* `Hyperf\Di\Container::class` -* `Hyperf\Contract\ContainerInterface::class` -* `Hypervel\Container\Contracts\Container::class` -* `Hypervel\Container\Container::class` -* `Hypervel\Foundation\Contracts\Application::class` -* `Hypervel\Foundation\Application::class` -::: - -### Automatic Injection - -Alternatively, and importantly, you may type-hint the dependency in the constructor of a class that is resolved by the container, including [controllers](/docs/controllers), [event listeners](/docs/events), [middleware](/docs/middleware), and more. In practice, this is how most of your objects should be resolved by the container. - -For example, you may type-hint a service defined by your application in a controller's constructor. The service will automatically be resolved and injected into the class: - -```php -apple->findPodcast($id); - } -} -``` - -## Method Invocation and Injection - -Sometimes you may wish to invoke a method on an object instance while allowing the container to automatically inject that method's dependencies. For example, given the following class: - -```php -app->resolving(Transistor::class, function (Transistor $transistor, Application $app) { - // Called when container resolves objects of type "Transistor"... -}); - -$this->app->resolving(function (mixed $object, Application $app) { - // Called when container resolves object of any type... -}); -``` - -As you can see, the object being resolved will be passed to the callback, allowing you to set any additional properties on the object before it is given to its consumer. - -## PSR-11 - -Hypervel's service container implements the [PSR-11](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-11-container.md) interface. Therefore, you may type-hint the PSR-11 container interface to obtain an instance of the Hypervel container: - -```php -use App\Services\Transistor; -use Psr\Container\ContainerInterface; - -Route::get('/', function (ContainerInterface $container) { - $service = $container->get(Transistor::class); - - // ... -}); -``` - -An exception is thrown if the given identifier can't be resolved. The exception will be an instance of `Psr\Container\NotFoundExceptionInterface` if the identifier was never bound. If the identifier was bound but was unable to be resolved, an instance of `Psr\Container\ContainerExceptionInterface` will be thrown. \ No newline at end of file diff --git a/_archive/docs/0.3/docs/context.md b/_archive/docs/0.3/docs/context.md deleted file mode 100644 index e700278a8e..0000000000 --- a/_archive/docs/0.3/docs/context.md +++ /dev/null @@ -1,127 +0,0 @@ -# Context -[[toc]] - -## Introduction - -Context is an important feature in Hypervel. It is used to store states within coroutines. Contexts in different coroutines are isolated, and when a coroutine terminates, its context will be destroyed automatically. You don't need to worry about memory leaks problem in context. - -If you're not familiar with the context management in coroutines, you can read [Isolating global variables with a coroutine context manager in Swoole](https://swoolelabs.com/blog/isolating-variables-with-coroutine-context) for more detailed information. - -::: important -Do not mutate static variables in coroutines unless you know what you're doing, because static variables are shared by all coroutines. It will cause states bleeding and unpredictable bugs. -::: - -## Interacting with Context - -### Get Context - -The `get` method will return the value of the key, if not exists, it will return the default value. - -```php -use Hypervel\Context\Context; - -$foo = Context::get('foo', 'bar'); - -$foo = Context::get('foo', 'bar', $coroutineId); -``` - -### Set Context - -The `set` method will set the value of the key, and return the value of the key. - -```php -use Hypervel\Context\Context; - -// $foo is bar -$foo = Context::set('foo', 'bar'); - -$foo = Context::set('foo', 'bar', $coroutineId); -``` - -### Check If Context Key Exists - -The `has` method will return true if the key exists, otherwise false. - -```php -use Hypervel\Context\Context; - -$exists = Context::has('foo'); - -$exists = Context::has('foo', $coroutineId); -``` - -### Override Context - -Sometimes we need to check if a specific key exists. If the key exists, override the value of a key in the context. You may do so using the `override` method. - -```php -use Hypervel\Context\Context; - -$request = Context::override(ServerRequestInterface::class, function (ServerRequestInterface $request) { - return $request->withAddedHeader('foo', 'bar'); -}); -``` - -::: note -You can pass `coroutineId` to the third parameter to override the context in a specific coroutine. -::: - -### Destroy Context - -The `destroy` method will delete the value of the key in coroutine context. - -```php -use Hypervel\Context\Context; - -Context::destroy('foo'); - -Context::destroy('foo', $coroutineId); -``` - -### Destroy All Context - -The `destroyAll` method will delete all the values in coroutine context. - -```php -use Hypervel\Context\Context; - -Context::destroyAll('foo'); - -Context::destroyAll('foo', $coroutineId); -``` - -### Get or Set Context - -The `getOrSet` method will return the value of the key, if not exists, it will set the value of the key and return. - -```php -use Hypervel\Context\Context; - -$foo = Context::getOrSet('foo', 'bar'); - -$foo = Context::getOrSet('foo', 'bar', $coroutineId); -``` - -### Copy Context - -The `copy` method will copy the context from another coroutine to the current coroutine. - -```php -use Hypervel\Context\Context; - -Context::copy('foo', $fromCoroutineId, $onlyKeys); -``` - -### Copy Context From Non Coroutine Environment - -For compatibility,`Context` also works in non-coroutine environment. All the non-coroutine environment will share the same context copy. - -The `copyFromNonCoroutine` method will copy the context from non-coroutine environment to current coroutine. - -```php -use Hypervel\Context\Context; - -Context::copy('copyFromNonCoroutine', $keys); - -Context::copy('copyFromNonCoroutine', $keys, $coroutineId); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/contracts.md b/_archive/docs/0.3/docs/contracts.md deleted file mode 100644 index 0d949cd343..0000000000 --- a/_archive/docs/0.3/docs/contracts.md +++ /dev/null @@ -1,137 +0,0 @@ -# Contracts -[[toc]] - -## Introduction - -Hypervel's "contracts" are a set of interfaces that define the core services provided by the framework. For example, an `Hypervel\Auth\Contracts\Gate` contract defines the methods needed for authorizing a resource, while the `Hypervel\Hashing\Contracts\Hasher` contract defines the methods needed for generating a secure hash. - -All of the contracts live separately in the `Contracts` directory in their belonging packages. This provides a quick reference point for all available contracts, as well as a single, decoupled package that may be utilized when building packages that interact with Hypervel services. - -### Contracts vs. Facades - -Hypervel's [facades](/docs/facades) and helper functions provide a simple way of utilizing Hypervel's services without needing to type-hint and resolve contracts out of the service container. In most cases, each facade has an equivalent contract. - -Unlike facades, which do not require you to require them in your class' constructor, contracts allow you to define explicit dependencies for your classes. Some developers prefer to explicitly define their dependencies in this way and therefore prefer to use contracts, while other developers enjoy the convenience of facades. **In general, most applications can use facades without issue during development.** - -## When to Use Contracts - -The decision to use contracts or facades will come down to personal taste and the tastes of your development team. Both contracts and facades can be used to create robust, well-tested Hypervel applications. Contracts and facades are not mutually exclusive. Some parts of your applications may use facades while others depend on contracts. As long as you are keeping your class' responsibilities focused, you will notice very few practical differences between using contracts and facades. - -In general, most applications can use facades without issue during development. If you are building a package that integrates with multiple PHP frameworks you may wish to use the corresponding contracts to define your integration with Hypervel's services without the need to require Hypervel's concrete implementations in your package's `composer.json` file. - -## How to Use Contracts - -So, how do you get an implementation of a contract? It's actually quite simple. - -Many types of classes in Hypervel are resolved through the [service container](/docs/container), including controllers, event listeners, middleware and even route closures. So, to get an implementation of a contract, you can just "type-hint" the interface in the constructor of the class being resolved. - -For example, take a look at this event listener: - -```php - -## Contract Reference - -This table provides a quick reference to all of the Hypervel contracts and their equivalent facades: - -| Contract | References Facade | -|--------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| -| [Hypervel\Auth\Contracts\Authorizable](https://github.com/hypervel/components/blob/main/src/auth/src/Contracts/Authorizable.php) |   | -| [Hypervel\Auth\Contracts\Gate](https://github.com/hypervel/components/blob/main/src/auth/src/Contracts/Gate.php) | `Gate` | -| [Hypervel\Auth\Contracts\Authenticatable](https://github.com/hypervel/components/blob/main/src/auth/src/Contracts/Authenticatable.php) |   | -| [Hypervel\Auth\Contracts\Factory](https://github.com/hypervel/components/blob/main/src/auth/src/Contracts/FactoryContract.php) | `Auth` | -| [Hypervel\Auth\Contracts\Guard](https://github.com/hypervel/components/blob/main/src/auth/src/Contracts/Guard.php) | `Auth::guard()` | -| [Hypervel\Auth\Contracts\StatefulGuard](https://github.com/hypervel/components/blob/main/src/auth/src/Contracts/StatefulGuard.php) |   | -| [Hypervel\Auth\Contracts\SupportsBasicAuth](https://github.com/hypervel/components/blob/main/src/auth/src/Contracts/SupportsBasicAuth.php) |   | -| [Hypervel\Auth\Contracts\UserProvider](https://github.com/hypervel/components/blob/main/src/auth/src/Contracts/UserProvider.php) |   | -| [Hypervel\Bus\Contracts\Dispatcher](https://github.com/hypervel/components/blob/main/src/bus/src/Contracts/Dispatcher.php) | `Bus` | -| [Hypervel\Bus\Contracts\QueuingDispatcher](https://github.com/hypervel/components/blob/main/src/bus/src/Contracts/QueuingDispatcher.php) | `Bus::dispatchToQueue()` | -| [Hypervel\Broadcasting\Contracts\Factory](https://github.com/hypervel/components/blob/main/src/broadcasting/src/Contracts/Factory.php) | `Broadcast` | -| [Hypervel\Broadcasting\Contracts\Broadcaster](https://github.com/hypervel/components/blob/main/src/broadcasting/src/Contracts/Broadcaster.php) | `Broadcast::connection()` | -| [Hypervel\Broadcasting\Contracts\ShouldBroadcast](https://github.com/hypervel/components/blob/main/src/broadcasting/src/Contracts/ShouldBroadcast.php) |   | -| [Hypervel\Broadcasting\Contracts\ShouldBroadcastNow](https://github.com/hypervel/components/blob/main/src/broadcasting/src/Contracts/ShouldBroadcastNow.php) |   | -| [Hypervel\Cache\Contracts\Factory](https://github.com/hypervel/components/blob/main/src/cache/src/Contracts/Factory.php) | `Cache` | -| [Hypervel\Cache\Contracts\Lock](https://github.com/hypervel/components/blob/main/src/cache/src/Contracts/Lock.php) |   | -| [Hypervel\Cache\Contracts\LockProvider](https://github.com/hypervel/components/blob/main/src/cache/src/Contracts/LockProvider.php) |   | -| [Hypervel\Cache\Contracts\Repository](https://github.com/hypervel/components/blob/main/src/cache/src/Contracts/Repository.php) | `Cache::driver()` | -| [Hypervel\Cache\Contracts\Store](https://github.com/hypervel/components/blob/main/src/cache/src/Contracts/Store.php) |   | -| [Hypervel\Config\Contracts\Repository](https://github.com/hypervel/components/blob/main/src/config/src/Contracts/Repository.php) | `Config` | -| [Hypervel\Container\Contracts\Container](https://github.com/hypervel/components/blob/main/src/container/src/Contracts/Container.php) | `App` | -| [Hypervel\Foundation\Exceptions\Contracts\ExceptionHandler](https://github.com/hypervel/components/blob/main/src/foundation/src/Exceptions/Contracts/ExceptionHandler.php) |   | -| [Hypervel\Encryption\Contracts\Encrypter](https://github.com/hypervel/components/blob/main/src/encryption/src/Contracts/ReposEncrypteritory.php) | `Crypt` | -| [Hypervel\Event\Contracts\Dispatcher](https://github.com/hypervel/components/blob/main/src/event/src/Contracts/Dispatcher.php) | `Event` | -| [Hypervel\Filesystem\Contracts\Cloud](https://github.com/hypervel/components/blob/main/src/filesystem/src/Contracts/Cloud.php) | `Storage::cloud()` | -| [Hypervel\Filesystem\Contracts\Factory](https://github.com/hypervel/components/blob/main/src/filesystem/src/Contracts/Factory.php) | `Storage` | -| [Hypervel\Filesystem\Contracts\Filesystem](https://github.com/hypervel/components/blob/main/src/filesystem/src/Contracts/Filesystem.php) | `Storage::disk()` | -| [Hypervel\Foundation\Contracts\Application](https://github.com/hypervel/components/blob/main/src/foundation/src/Contracts/Application.php) | `App` | -| [Hypervel\Console\Contracts\Application](https://github.com/hypervel/components/blob/main/src/foundation/src/Console/Contracts/Application.php) |   | -| [Hypervel\Foundation\Console\Contracts\Kernel](https://github.com/hypervel/components/blob/main/src/foundation/src/Console/Contracts/Kernel.php) | `Artisan` | -| [Hypervel\Hashing\Contracts\Hasher](https://github.com/hypervel/components/blob/main/src/hashing/src/Contracts/Hasher.php) | `Hash` | -| [Hypervel\Mail\Contracts\MailQueue](https://github.com/hypervel/components/blob/main/src/mail/src/Contracts/MailQueue.php) | `Mail::queue()` | -| [Hypervel\Mail\Contracts\Mailable](https://github.com/hypervel/components/blob/main/src/mail/src/Contracts/Mailable.php) |   | -| [Hypervel\Mail\Contracts\Mailer](https://github.com/hypervel/components/blob/main/src/mail/src/Contracts/Mailer.php) | `Mail` | -| [Hypervel\Notifications\Contracts\Dispatcher](https://github.com/hypervel/components/blob/main/src/notifications/src/Contracts/Dispatcher.php) | `Notification` | -| [Hypervel\Notifications\Contracts\Factory](https://github.com/hypervel/components/blob/main/src/notifications/src/Contracts/Factory.php) | `Notification` | -| [Hypervel\ObjectPool\Contracts\Factory](https://github.com/hypervel/components/blob/main/src/object-pool/src/Contracts/Factory.php) |   | -| [Hypervel\ObjectPool\Contracts\ObjectPool](https://github.com/hypervel/components/blob/main/src/object-pool/src/Contracts/ObjectPool.php) |   | -| [Hypervel\ObjectPool\Contracts\Recycler](https://github.com/hypervel/components/blob/main/src/object-pool/src/Contracts/Recycler.php) |   | -| [Hypervel\Queue\Contracts\EntityResolver](https://github.com/hypervel/components/blob/main/src/queue/src/Contracts/EntityResolver.php) |   | -| [Hypervel\Queue\Contracts\Factory](https://github.com/hypervel/components/blob/main/src/queue/src/Contracts/Factory.php) | `Queue` | -| [Hypervel\Queue\Contracts\Job](https://github.com/hypervel/components/blob/main/src/queue/src/Contracts/Job.php) |   | -| [Hypervel\Queue\Contracts\Monitor](https://github.com/hypervel/components/blob/main/src/queue/src/Contracts/Factory.php) | `Queue` | -| [Hypervel\Queue\Contracts\Queue](https://github.com/hypervel/components/blob/main/src/queue/src/Contracts/Queue.php) | `Queue::connection()` | -| [Hypervel\Queue\Contracts\QueueableCollection](https://github.com/hypervel/components/blob/main/src/queue/src/Contracts/QueueableCollection.php) |   | -| [Hypervel\Queue\Contracts\QueueableEntity](https://github.com/hypervel/components/blob/main/src/queue/src/Contracts/QueueableEntity.php) |   | -| [Hypervel\Queue\Contracts\ShouldQueue](https://github.com/hypervel/components/blob/main/src/queue/src/Contracts/ShouldQueue.php) |   | -| [Hypervel\Router\Contracts\UrlGenerator](https://github.com/hypervel/components/blob/main/src/router/contracts/UrlGenerator.php) |   | -| [Hypervel\Router\Contracts\UrlRoutable](https://github.com/hypervel/components/blob/main/src/router/contracts/UrlRoutable.php) |   | -| [Hypervel\Session\Contracts\Factory](https://github.com/hypervel/components/blob/main/src/session/contracts/Factory.php) |   | -| [Hypervel\Session\Contracts\Session](https://github.com/hypervel/components/blob/main/src/session/contracts/Session.php) | `Session::store()` | -| [Hypervel\Support\Contracts\Arrayable](https://github.com/hypervel/components/blob/main/src/support/contracts/Arrayable.php) |   | -| [Hypervel\Support\Contracts\Htmlable](https://github.com/hypervel/components/blob/main/src/support/contracts/Htmlable.php) |   | -| [Hypervel\Support\Contracts\Jsonable](https://github.com/hypervel/components/blob/main/src/support/contracts/Jsonable.php) |   | -| [Hypervel\Support\Contracts\MessageBag](https://github.com/hypervel/components/blob/main/src/support/contracts/MessageBag.php) |   | -| [Hypervel\Support\Contracts\MessageProvider](https://github.com/hypervel/components/blob/main/src/support/contracts/MessageProvider.php) |   | -| [Hypervel\Support\Contracts\Renderable](https://github.com/hypervel/components/blob/main/src/support/contracts/Renderable.php) |   | -| [Hypervel\Support\Contracts\Reasonable](https://github.com/hypervel/components/blob/main/src/support/contracts/Reasonable.php) |   | -| [Hypervel\Translation\Contracts\Loader](https://github.com/hypervel/components/blob/main/src/translation/contracts/Loader.php) |   | -| [Hypervel\Translation\Contracts\Translator](https://github.com/hypervel/components/blob/main/src/translation/contracts/Translator.php) | `Lang` | -| [Hypervel\Foundation\Exceptions\Contracts\ExceptionHandler](https://github.com/hypervel/components/blob/main/src/foundation/src/Exceptions/Contracts/ExceptionHandler.php) |   | -| [Hypervel\Foundation\Exceptions\Contracts\ExceptionRenderer](https://github.com/hypervel/components/blob/main/src/foundation/src/Exceptions/Contracts/ExceptionRenderer.php) |   | -| [Hypervel\Foundation\Http\Contracts\ExceptionRenderer](https://github.com/hypervel/components/blob/main/src/foundation/src/Http/Contracts/MiddlewareContract.php) |   | -| [Hypervel\Cookie\Contracts\Cookie](https://github.com/hypervel/components/blob/main/src/cookie/src/Contracts/Cookie.php) | `Cookie` | -| [Hypervel\Http\Contracts\RequestContract](https://github.com/hypervel/components/blob/main/src/http/src/Contracts/RequestContract.php) | `Request` | -| [Hypervel\Http\Contracts\ResponseContract](https://github.com/hypervel/components/blob/main/src/http/src/Contracts/ResponseContract.php) | `Response` | -| [Hypervel\Router\Contracts\UrlGenerator](https://github.com/hypervel/components/blob/main/src/router/src/Contracts/UrlGenerator.php) | `URL` | - -::: info -The contracts in Hyperf can refer to [hyperf/contract](https://github.com/hyperf/hyperf/tree/master/src/contract/src) package -::: \ No newline at end of file diff --git a/_archive/docs/0.3/docs/contributions.md b/_archive/docs/0.3/docs/contributions.md deleted file mode 100644 index d53001af47..0000000000 --- a/_archive/docs/0.3/docs/contributions.md +++ /dev/null @@ -1,86 +0,0 @@ -# Contribution Guide -[[toc]] - -## Bug Reports - -To encourage active collaboration, Hypervel strongly encourages pull requests, not just bug reports. Pull requests will only be reviewed when marked as "ready for review" (not in the "draft" state) and all tests for new features are passing. Lingering, non-active pull requests left in the "draft" state will be closed after a few days. - -However, if you file a bug report, your issue should contain a title and a clear description of the issue. You should also include as much relevant information as possible and a code sample that demonstrates the issue. The goal of a bug report is to make it easy for yourself - and others - to replicate the bug and develop a fix. - -Remember, bug reports are created in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the bug report will automatically see any activity or that others will jump to fix it. Creating a bug report serves to help yourself and others start on the path of fixing the problem. If you want to chip in, you can help out by fixing [any bugs listed in our issue trackers](https://github.com/hypervel/components/issues). You must be authenticated with GitHub to view all of Hypervel's issues. - -If you notice improper DocBlock, PHPStan, or IDE warnings while using Hypervel, do not create a GitHub issue. Instead, please submit a pull request to fix the problem. - -## Support Questions - -Hypervel's GitHub issue trackers are not intended to provide Hypervel help or support. Instead, use [GitHub Discussions](https://github.com/hypervel/components/discussions) - -## Which Branch? - -All bug fixes should be sent to the latest version that supports bug fixes (currently **0.1**). Bug fixes should never be sent to the **main** branch unless they fix features that exist only in the upcoming release. - -**Minor** features that are **fully backward compatible** with the current release may be sent to the latest stable branch (currently 0.1). - -**Major new** features or features with breaking changes should always be sent to the **main** branch, which contains the upcoming release. - -## Security Vulnerabilities - -If you discover a security vulnerability within Hypervel, please send an email to Albert Chen at [albert@hypervel.org](mailto:albert@hypervel.org). All security vulnerabilities will be promptly addressed. - -## Coding Style - -Hypervel follows the [PSR-12](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide-meta.md) coding standard and the [PSR-4](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md) autoloading standard. - -### Strict Types - -Always use native types for type hinting, and declare strict types at the beginning of PHP files. - -```php -declare(strict_types=1); - -/** - * Execute the job. - */ -public function handle(AudioProcessor $processor): void -{ - // -} -``` - -### PHPDoc - -Below is an example of a valid Hypervel documentation block if you need to mark additional info for the type hinting. Note that the `@param` attribute is followed by two spaces, the argument type, two more spaces, and finally the variable name: - -```php -/** - * E-mail the results of the scheduled operation if it fails. - * - * @param array|mixed $addresses - */ -public function emailOutputOnFailure(mixed $addresses): static -``` - -And when the native type is generic, please specify the generic type through the use of the `@param` or `@return` attributes: - -```php -/** - * Get the attachments for the message. - * - * @return array - */ -public function attachments(): array -{ - return [ - Attachment::fromStorage('/path/to/file'), - ]; -} -``` - -## Code of Conduct - -The Hypervel code of conduct is derived from the Laravel code of conduct. Any violations of the code of conduct may be reported to Albert Chen (albert@hypervel.org): - -* Participants will be tolerant of opposing views. -* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. -* When interpreting the words and actions of others, participants should always * assume good intentions. -* Behavior that can be reasonably considered harassment will not be tolerated. \ No newline at end of file diff --git a/_archive/docs/0.3/docs/controllers.md b/_archive/docs/0.3/docs/controllers.md deleted file mode 100644 index 1d8d43084c..0000000000 --- a/_archive/docs/0.3/docs/controllers.md +++ /dev/null @@ -1,188 +0,0 @@ -# Controllers -[[toc]] - -## Introduction - -Instead of defining all of your request handling logic as closures in your route files, you may wish to organize this behavior using "controller" classes. Controllers can group related request handling logic into a single class. For example, a `UserController` class might handle all incoming requests related to users, including showing, creating, updating, and deleting users. By default, controllers are stored in the `app/Http/Controllers` directory. - -## Writing Controllers - -### Basic Controllers - -To quickly generate a new controller, you may run the `make:controller` Artisan command. By default, all of the controllers for your application are stored in the `app/Http/Controllers` directory: - -```shell -php artisan make:controller UserController -``` - -Let's take a look at an example of a basic controller. A controller may have any number of public methods which will respond to incoming HTTP requests: - -```php - User::findOrFail($id) - ]); - } -} -``` - -Once you have written a controller class and method, you may define a route to the controller method like so: - -```php -use App\Http\Controllers\UserController; - -Route::get('/user/{id}', [UserController::class, 'show']); -``` - -When an incoming request matches the specified route URI, the `show` method on the `App\Http\Controllers\UserController` class will be invoked and the route parameters will be passed to the method. - -::: note -Controllers are not **required** to extend a base class. However, it is sometimes convenient to extend a base controller class that contains methods that should be shared across all of your controllers. -::: - -### Single Action Controllers - -If a controller action is particularly complex, you might find it convenient to dedicate an entire controller class to that single action. To accomplish this, you may define a single `__invoke` method within the controller: - -```php - 'auth']); -``` - -## Dependency Injection and Controllers - -#### Constructor Injection - -The Hypervel [service container](/docs/container) is used to resolve all Hypervel controllers. As a result, you are able to type-hint any dependencies your controller may need in its constructor. The declared dependencies will automatically be resolved and injected into the controller instance: - -```php -name; - - // Store the user... - - return ['success' => true]; - } -} -``` - -If your controller method is also expecting input from a route parameter, list your route arguments after your other dependencies. For example, if your route is defined like so: - -```php -use App\Http\Controllers\UserController; - -Route::put('/user/{id}', [UserController::class, 'update']); -``` - -You may still type-hint the `Hypervel\Http\Request` and access your `id` parameter by defining your controller method as follows: - -```php - true]; - } -} -``` - -## Proxying Controller Methods - -Sometimes you may want to execute some logics before calling controller actions or redefine the action target. You can define `callAction` method in your controller: - -```php -public function callAction(string $action, array $parameters) -{ - if ($oldMethod = $this->oldVersionMethod('action')) { - return $this->{$oldMethod}($parameters); - } - - return $this->{$action}($parameters); -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/coroutine.md b/_archive/docs/0.3/docs/coroutine.md deleted file mode 100644 index 07ae57eef4..0000000000 --- a/_archive/docs/0.3/docs/coroutine.md +++ /dev/null @@ -1,540 +0,0 @@ -# Coroutine -[[toc]] - -## Introduction - -In traditional PHP-FPM environments, I/O operations are blocking by nature. This means worker processes remain idle while waiting for I/O responses, leading to inefficient resource utilization. The conventional approach to enhance concurrency is running multiple processes simultaneously, as demonstrated in Laravel's [Concurrency](https://laravel.com/docs/concurrency) feature. - -However, process-based concurrency has significant drawbacks. The overhead of context switching between processes is substantial, and the concurrent capacity is constrained by available CPU cores. Essentially, improving concurrency requires either vertical scaling (more powerful hardware) or horizontal scaling (more servers). Even solutions like [Laravel Octane](https://laravel.com/docs/octane) cannot fundamentally address these limitations in I/O-intensive scenarios. - -> For a detailed comparison, see [Why Hypervel?](/docs/introduction#why-hypervel) - -Unlike Laravel, Hypervel supports coroutine capabilities out of the box. All th I/O operations are non-blocking I/O throughout the framework. Hypervel achieves true concurrent request processing within each worker process. When a coroutine encounters an I/O operation, instead of blocking the entire worker process, it yields control to other coroutines, allowing the worker to continue processing additional requests efficiently. - -## What is Coroutine? - -Coroutines are functions that can pause their execution and later resume from where they left off, maintaining their state between pauses. Think of them as functions with multiple entry and exit points. - -The key mechanism behind coroutines is a concept called "cooperative multitasking." Unlike processes or threads where the operating system controls when to switch between tasks, coroutines voluntarily yield control at specific points in their code. This is typically done at I/O operations or when explicitly yielding. - -When a coroutine encounters an operation that would normally block (like waiting for network data), instead of blocking the entire program, it yields control back to a scheduler. The scheduler can then run other coroutines until the blocking operation completes. - -The cost for each coroutine is extremely lightweight compared to processes or threads, making them ideal for concurrent I/O operations. - -::: info -For PHP developers who have never been exposed to the concept of Coroutines and have no experience with other related programming languages, I strongly recommend learning about Linux operating systems, asynchronous I/O, coroutines, network communication protocols, concurrency, and other fundamental knowledge through [Mastering Swoole PHP](https://swoolebook.com/). This will help you gain a more comprehensive understanding of how Coroutines work behind the scenes when you use them in Hypervel. -::: - -### How Coroutines Work - -Coroutines achieve concurrency without parallelism. They allow interleaving multiple tasks within a single thread by: - -1. Saving the current execution state (variables, program counter) -2. Switching to another coroutine -3. Restoring the previous state when resuming - -This approach eliminates the overhead of thread creation, context switching, and synchronization mechanisms required in multithreading. It also avoids the complexity of managing shared state between threads. - -Coroutines are particularly efficient for I/O-bound operations where programs spend significant time waiting for external resources rather than performing CPU-intensive calculations. - -::: info -For more detailed information, see [Coroutine](https://en.wikipedia.org/wiki/Coroutine). -::: - -## Coroutine Features - -Coroutines in Hypervel operate within a coroutine container environment. By default, the framework automatically initializes these containers for: - -* HTTP requests -* Console commands - -In most scenarios, you won't need to manually create coroutine containers as they're handled automatically by the framework. - -All the coroutine-related methods in Hypervel are defined in `Hypervel\Coroutine\Coroutine`, and functions in `Hypervel\Coroutine` namespace. - -### Creating Container Manually - -For scenarios requiring explicit coroutine container creation (e.g., unit tests without a coroutine environment), you can use the `Hypervel\Coroutine\run` function: - -```php -use Hypervel\Coroutine\Coroutine; - -use function Hypervel\Coroutine\run; - -echo 'My coroutine id: ' . Coroutine::id() . PHP_EOL; - -run(function () { - echo 'My coroutine id: ' . Coroutine::id(); -}); -``` - -### Getting Coroutine Id - -Each coroutine in Hypervel has a coroutine id. When executing within a coroutine context: - -* The ID is a positive integer -* IDs are auto-incrementing as new coroutines are created - -You can retrieve the current coroutine ID using: - -```php -use Hypervel\Coroutine\Coroutine; - -$id = Coroutine::id(); -``` - -If `Coroutine::id()` returns `-1`, it indicates that the code is executing outside of a coroutine context. This is common in traditional PHP-FPM environments or running code before coroutine container initialization. - -### Determine if in Coroutine - -You can also use `inCoroutine` method to determine if you're in coroutines: - -```php -use Hypervel\Coroutine\Coroutine; - -use function Hypervel\Coroutine\run; - -echo 'in coroutine: ' . (int) Coroutine::inCoroutine() . PHP_EOL; - -run(function () { - echo 'in coroutine : ' . (int) Coroutine::inCoroutine(); -}); -``` - -## Creating a Coroutine - -Creating a coroutine in Hypervel is as easy as pie. If you have development experience in Golang, you will be pretty similar to its syntax: - -```php -use function Hypervel\Coroutine\go; - -go(function () { - sleep(1); - echo 'In coroutine' . PHP_EOL; -}); - -echo 'Hello world!' . PHP_EOL; -``` - -You can create coroutines simply by `go` function. Process will automatically yield out when there's I/O happening in coroutines. And when the I/O result is returned, the process will resume the coroutine and executing the rest of the code. - -In the above example, you will get the following result: - -``` -Hello world! -In coroutine -``` - -`Hello world!` will be printed first, `In coroutine` will comes out after 1 second. This basic example fully demonstrates how coroutine works. - -Besides `go` function, you can also create a coroutine through `Hypervel\Coroutine\Coroutine` class: - -```php -use Hypervel\Coroutine\Coroutine; - -Coroutine::create(function () { - sleep(1); - echo 'In coroutine' . PHP_EOL; -}); - -echo 'Hello world!' . PHP_EOL; -``` - -### Nested Coroutines - -Coroutines can be nested, enabling the creation of coroutines inside others: - -```php -use function Hypervel\Coroutine\go; - -go(function () { - echo 'In parent coroutine' . PHP_EOL; - - go(function () { - sleep(1); - echo 'In nested coroutine' . PHP_EOL; - }); - - echo 'Back to parent coroutine' . PHP_EOL; -}); - -echo 'Main process' . PHP_EOL; -``` - -Output will be: - -``` -Main Process -In parent coroutine -Back to parent coroutine -In nested coroutine -``` - -Each nested coroutine: - -* Gets its own coroutine ID -* Has independent execution flow -* Has its own context storage (context are separated) -* Can be created to any depth (within memory constraints) - -### Error Handling in Coroutines - -The key principle to remember is that a try/catch block should only operate within a single coroutine. Think of a coroutine as an isolated try/catch cannot span across multiple coroutines. This is because coroutines execute independently in their own contexts, separate from where the try/catch is defined, making it impossible to catch exceptions thrown in different coroutines. - -#### Wrong Way to Handle Throwables - -```php -use function Hypervel\Coroutine\go; - -try { - go(function () { - throw new \RuntimeException('test'); - }); -} catch (\Throwable $e) { - echo $e; -} -``` - -The example above is wrong because it creates a new coroutine context inside the try block. This approach is problematic since any errors occurring within the coroutine won't be caught—the call to `go()` returns immediately and execution continues. Exceptions must be thrown and caught within the same coroutine, not across different ones. - -::: info -It's highly recommended to set `swoole.use_shortname` to `Off` in your `php.ini`. If you didn't turn off `swoole.use_shortname` and use global coroutine functions without the namespace, errors occurring in coroutines will not be reported by exception handler. Unhandled errors will be printed out to the console. -::: - -#### Right Way to Handle Throwables - -```php -push('Hello from coroutine!'); -}); - -go(function () use ($channel) { - $data = $channel->pop(); - // Outputs: Hello from coroutine! - echo $data; -}); -``` - -Channels can be buffered or unbuffered: -* Buffered channels (buffer size > 0): Push operations won't block until the buffer is full -* Unbuffered channels (buffer size = 0): Push operations block until another coroutine pops the data - -#### Pub/Sub Pattern - -Channels can be conceptualized as an implementation of the publish-subscribe (pub/sub) pattern. In this model: - -* **Publishers (producers)**: push data to the channel -* **Subscribers (consumers)**: receive data from the channel - -Here's a practical example of using channels for job processing: - -```php -use Hypervel\Coroutine\Channel; - -use function Hypervel\Coroutine\go; - -class JobProcessor -{ - public function process(array $jobs) - { - // Buffer 10 jobs - $channel = new Channel(10); - - // Producer: Send jobs to channel - go(function () use ($channel, $jobs) { - foreach ($jobs as $job) { - $channel->push($job); - } - // Signal no more jobs - $channel->close(); - }); - - // Consumer: Process jobs from channel - go(function () use ($channel) { - while (true) { - $job = $channel->pop(); - // Channel closed - if ($job === false) { - break; - } - // Process job - $this->processJob($job); - } - }); - } -} -``` - -The publish-subscribe model through channels is particularly useful for event-driven architectures and distributing work among multiple coroutines. - -### Defer - -The `defer` function allows you to schedule a function to be executed when the current coroutine exits. It's a powerful coroutine feature that ensures certain code executes when a coroutine terminates, regardless of how it terminates (normally or by exception). This is similar to `try-finally` blocks but specific to coroutines. - -Key aspects of `defer` are: - -* **Guaranteed execution**: The deferred function will run when the coroutine exits, whether through normal completion, cancellation, or an uncaught exception -* **Resource management**: Ideal for cleaning up resources (closing files, disconnecting from services, etc.) -* **Last-in, first-out order**: Multiple defers execute in reverse order of their registration (like a stack) -* **Execution context**: Deferred functions run in the same context as the coroutine - -```php -use function Hypervel\Coroutine\defer; - -defer(function () { - echo 'Cleanup 1' . PHP_EOL; -}); - -defer(function () { - echo 'Cleanup 2' . PHP_EOL; -}); - -echo 'Main logic'. PHP_EOL; -``` - -The eventual output will be: - -``` -Main logic -Cleanup2 -Cleanup1 -``` - -Multiple defers are executed in LIFO (Last In, First Out) order. - -#### Error handling in Defer - -Sometimes exceptions may happen during the defer. In this case you may try to catch exceptions like below: - -```php -try { - defer(function () { - throw new Exception('defer error'); - }); - echo 'Main logic' . PHP_EOL; -} catch (Throwable $e) { - echo $e->getMessage() . PHP_EOL; -} -``` - -However, it's not going to work in this case. This is because the `defer` function doesn't execute the provided callback immediately. Instead, it schedules the callback to run at the end of the current coroutine. By the time the deferred function executes and throws the exception, the program has already left the try-catch block. - -To handle exceptions in deferred functions, you should implement error handling within the deferred function itself: - -```php -defer(function () { - try { - // Code that might throw an exception - throw new Exception('defer error'); - } catch (Throwable $e) { - echo $e->getMessage() . PHP_EOL; - } -}); -echo 'Main logic' . PHP_EOL; -``` - -### WaitGroup - -WaitGroup is a synchronization primitive that allows one coroutine to wait for the completion of a collection of coroutines. It works like a counter that tracks active coroutines: - -This pattern is particularly useful when you need to: -* Launch a dynamic number of concurrent operations -* Ensure all operations complete before proceeding -* Avoid complex channel management for simple synchronization - -```php -use Hypervel\Coroutine\WaitGroup; - -use function Hypervel\Coroutine\go; - -$waiter = new WaitGroup(); - -for ($i = 0; $i < 3; $i++) { - $waiter->add(1); - go(function () use ($waiter, $i) { - // Do some work - sleep(1); - echo "Task {$i} completed\n"; - $waiter->done(); - }); -} - -// Wait for all coroutines to complete -$waiter->wait(); -echo "All tasks completed\n"; -``` - -Here's an example using `WaitGroup` for concurrent data processing in common use cases: - -```php -use Hypervel\Coroutine\WaitGroup; - -use function Hypervel\Coroutine\go; - -class DataProcessor -{ - public function processItems(array $items) - { - $results = []; - $waiter = new WaitGroup(); - - foreach ($items as $item) { - $waiter->add(1); - go(function () use ($waiter, $item, &$results) { - try { - $result = $this->processItem($item); - $results[] = [ - 'item' => $item, - 'status' => 'success', - 'result' => $result - ]; - } catch (\Exception $e) { - $results[] = [ - 'item' => $item, - 'status' => 'error', - 'error' => $e->getMessage() - ]; - } finally { - $waiter->done(); - } - }); - } - - $waiter->wait(); - return $results; - } -} -``` - -### Parallel - -The `parallel` function provides a convenient way to run multiple tasks concurrently and wait for all of them to complete. You can use `parallel` to replace `WaitGroup` in most cases for more convenient and concise usage: - -```php -use function Hypervel\Coroutine\parallel; - -$results = parallel([ - function () { - sleep(2); - return 'Task 1'; - }, - function () { - sleep(1); - return 'Task 2'; - } -]); -``` - -Results will contain `['Task 1', 'Task 2']`. - -### Concurrent - -The `concurrent` function allows you to limit the number of concurrent coroutines. This function provides a controlled way to execute multiple coroutines simultaneously while enforcing a maximum concurrency limit. When you have many coroutines that could potentially run at once, using `concurrent` helps you: - -1. **Manage resource usage**: Prevent system overload by limiting how many operations happen at once -2. **Control throughput**: Balance between speed and system stability -3. **Implement rate limiting**: Useful when working with APIs or services that have request limits - -```php -use Hypervel\Coroutine\Concurrent; - -// Process jobs with max 10 concurrent coroutines -$concurrent = new Concurrent(10); -foreach ($jobs as $job) { - // It will block here if there are already 10 jobs handling - $concurrent->create( - fn () => $job->execute() - ); -} -``` - -### Context - -One of the most important aspects of coroutines is `context isolation`. Each coroutine has its own independent context, which means context values in one coroutine are completely isolated from other coroutines. This isolation is crucial for maintaining data consistency and preventing unexpected behavior in concurrent applications. - -::: note -This is the main reason why Laravel can't really adopt full coroutines. All states in Laravel's components are stored as variables in objects, when these objects are shared by different coroutines, states will be a mess, then cause unexpected results. -::: - -For example, consider a web application handling multiple requests concurrently: - -```php -use Hypervel\Coroutine\Context; - -use function Hypervel\Coroutine\go; - -// Coroutine 1 handling User A's request -go(function () { - Context::set('user', 'User A'); - sleep(1); // Simulate some processing - // Outputs: User A - echo 'Coroutine 1: ' . Context::get('user') . PHP_EOL; -}); - -// Coroutine 2 handling User B's request -go(function () { - Context::set('user', 'User B'); - // Outputs: User B - echo 'Coroutine 2: ' . Context::get('user') . PHP_EOL; -}); -``` - -In this example: -* Each coroutine maintains its own context -* Changes to context in one coroutine don't affect other coroutines -* No need for locks or mutexes to prevent data races -* Memory is properly isolated between different requests - -This context isolation is particularly important when: -* Handling multiple HTTP requests simultaneously -* Processing concurrent database operations -* Managing user sessions or request-specific data -* Dealing with authentication or user-specific information - -You can see [Context](/docs/context) for full usages of context. - -## Common Pitfalls - -1. **Global State**: Avoid using global variables as they're shared between coroutines -2. **Resource Handling**: Always close resources properly -3. **Blocking Operations**: Most of stream-based I/O operations in extensions can be automatically hooked as coroutines by Swoole. But there might be some PHP extensions may block the entire process, like: mongoDB. - -By following these guidelines and understanding how coroutines work, you can build efficient, concurrent applications with Hypervel. diff --git a/_archive/docs/0.3/docs/csrf.md b/_archive/docs/0.3/docs/csrf.md deleted file mode 100644 index 4e0ec3e0df..0000000000 --- a/_archive/docs/0.3/docs/csrf.md +++ /dev/null @@ -1,104 +0,0 @@ -# CSRF Protection -[[toc]] - -## Introduction - -Cross-site request forgeries are a type of malicious exploit whereby unauthorized commands are performed on behalf of an authenticated user. Thankfully, Hypervel makes it easy to protect your application from [cross-site request forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery) (CSRF) attacks. - -### An Explanation of the Vulnerability - -In case you're not familiar with cross-site request forgeries, let's discuss an example of how this vulnerability can be exploited. Imagine your application has a `/user/email` route that accepts a `POST` request to change the authenticated user's email address. Most likely, this route expects an `email` input field to contain the email address the user would like to begin using. - -Without CSRF protection, a malicious website could create an HTML form that points to your application's `/user/email` route and submits the malicious user's own email address: - -```html -
    - -
    - - -``` - -## Preventing CSRF Requests - -Hypervel automatically generates a CSRF "token" for each active [user session](/docs/session) managed by the application. This token is used to verify that the authenticated user is the person actually making the requests to the application. Since this token is stored in the user's session and changes each time the session is regenerated, a malicious application is unable to access it. - -The current session's CSRF token can be accessed via the request's session or via the `csrf_token` helper function: - -```php -use Hypervel\Http\Request; - -Route::get('/token', function (Request $request) { - $token = $request->session()->token(); - - $token = csrf_token(); - - // ... -}); -``` - -Anytime you define a "POST", "PUT", "PATCH", or "DELETE" HTML form in your application, you should include a hidden CSRF `_token` field in the form so that the CSRF protection middleware can validate the request. For convenience, you may use the `@csrf` Blade directive to generate the hidden token input field: - -```html -
    - @csrf - - - -
    -``` - -The `App\Http\Middleware\VerifyCsrfToken` [middleware](/docs/middleware), which is included in the `web` middleware group by default, will automatically verify that the token in the request input matches the token stored in the session. When these two tokens match, we know that the authenticated user is the one initiating the request. - -### Excluding URIs From CSRF Protection - -Sometimes you may wish to exclude a set of URIs from CSRF protection. You should place these kinds of routes outside of the `web` middleware group that the `App\Providers\RouteServiceProvider` applies to all routes in the `routes/web.php` file. However, you may also exclude the routes by adding their URIs to the `$except` property of the `VerifyCsrfToken` middleware: - -```php - -``` - -Then, you can instruct a library like jQuery to automatically add the token to all request headers. This provides simple, convenient CSRF protection for your AJAX based applications using legacy JavaScript technology: - -```html -$.ajaxSetup({ - headers: { - 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') - } -}); -``` - -## X-XSRF-TOKEN - -Hypervel stores the current CSRF token in an `XSRF-TOKEN` cookie that is included with each response generated by the framework. You can use the cookie value to set the `X-XSRF-TOKEN` request header. - -This cookie is primarily sent as a developer convenience since some JavaScript frameworks and libraries, like Angular and Axios, automatically place its value in the `X-XSRF-TOKEN` header on same-origin requests. \ No newline at end of file diff --git a/_archive/docs/0.3/docs/data-objects.md b/_archive/docs/0.3/docs/data-objects.md deleted file mode 100644 index 0c29a8a81b..0000000000 --- a/_archive/docs/0.3/docs/data-objects.md +++ /dev/null @@ -1,705 +0,0 @@ -# Data Objects -[[toc]] - -## Introduction - -Data Objects provide a powerful and elegant way to create immutable, type-safe data transfer objects in your Hypervel application. They are perfect for handling API responses, configuration data, and any scenario where you need structured data with automatic type conversion and serialization capabilities. - -Unlike simple arrays or standard PHP objects, Data Objects offer automatic type casting, property name conversion, nested object resolution, and built-in serialization support while providing array access immutability to prevent accidental modification through array syntax. - -Key benefits of Data Objects include: - -- **Type Safety**: Automatic conversion between data types with validation -- **Array Access Protection**: Prevents modification through array access syntax (use `readonly` properties for full immutability) -- **Property Mapping**: Automatic conversion between snake_case and camelCase naming conventions -- **Nested Resolution**: Support for automatically resolving nested Data Objects and DateTime instances -- **Serialization**: Built-in JSON and array serialization support - -## Basic Usage - -### Creating a Data Object - -To create a Data Object, extend the `Hypervel\Support\DataObject` class and define your constructor with typed properties: - -```php - 'John Doe', - 'age' => '30', // String will be converted to int - 'email' => 'john@example.com', - 'phone' => null -]; - -$user = UserDataObject::make($userData); - -$user->name; // 'John Doe' -$user->age; // 30 (integer) -$user->email; // 'john@example.com' -$user->phone; // null -``` - -### Property Name Conversion - -Data Objects automatically convert between `snake_case` data keys and `camelCase` properties: - -```php -class ProductDataObject extends DataObject -{ - public function __construct( - public string $productName, // Maps to 'product_name' in array - public float $unitPrice, // Maps to 'unit_price' in array - public bool $isAvailable // Maps to 'is_available' in array - ) {} -} - -$product = ProductDataObject::make([ - 'product_name' => 'Laptop', - 'unit_price' => '999.99', - 'is_available' => 1 // Will be converted to true -]); - -$product->productName; // 'Laptop' -$product->unitPrice; // 999.99 (float) -$product->isAvailable; // true (boolean) - -$product['product_name']; // 'Laptop' -$product['unit_price']; // 999.99 (float) -$product['is_available']; // true (boolean) -``` - -## Type Conversion - -Data Objects automatically convert data types based on your property declarations: - -### Supported Conversions - -```php -class TypeConversionExample extends DataObject -{ - public function __construct( - public string $stringValue, - public int $intValue, - public float $floatValue, - public bool $boolValue, - public array $arrayValue - ) {} -} - -$data = TypeConversionExample::make([ - 'string_value' => 123, // Converted to '123' - 'int_value' => '42.99', // Converted to 42 (truncated) - 'float_value' => '3.14159', // Converted to 3.14159 - 'bool_value' => '0', // Converted to false - 'array_value' => 'single item' // Wrapped in ['single item'] -]); -``` - -### DateTime Handling - -Data Objects provide sophisticated DateTime handling: - -```php -class EventDataObject extends DataObject -{ - public function __construct( - public string $title, - public DateTime $createdAt - ) {} -} - -$event = EventDataObject::make([ - 'title' => 'Conference', - 'created_at' => '2023-12-25 10:00:00' // Converted to DateTime -], true); // autoResolve = true for DateTime conversion -``` - -## Nested Data Objects - -### Basic Nested Objects - -Data Objects support automatic resolution of nested objects when using `autoResolve = true`: - -```php -class AddressDataObject extends DataObject -{ - public function __construct( - public string $street, - public string $city, - public string $zipCode - ) {} -} - -class UserDataObject extends DataObject -{ - public function __construct( - public string $name, - public AddressDataObject|array|null $address - ) {} -} - -$userData = [ - 'name' => 'John Doe', - 'address' => [ - 'street' => '123 Main St', - 'city' => 'Boston', - 'zip_code' => '02101' - ] -]; - -// Without autoResolve (default) -$user1 = UserDataObject::make($userData); -// $user1->address is an array - -// With autoResolve -$user2 = UserDataObject::make($userData, true); -// $user2->address is an AddressDataObject instance -echo $user2->address->street; // '123 Main St' -``` - -### Deep Nested Objects - -AutoResolve works recursively for deeply nested structures: - -```php -class CompanyDataObject extends DataObject -{ - public function __construct( - public string $name, - public UserDataObject|array $employee - ) {} -} - -$companyData = [ - 'name' => 'Acme Corp', - 'employee' => [ - 'name' => 'Jane Smith', - 'address' => [ - 'street' => '456 Oak Ave', - 'city' => 'Boston', - 'zip_code' => '02101' - ] - ] -]; - -$company = CompanyDataObject::make($companyData, true); - -// All nested objects are automatically resolved -$company->employee->name; // 'Jane Smith' -$company->employee->address->street; // '456 Oak Ave' -``` - -## Array Access and Immutability - -Data Objects implement `ArrayAccess` for convenient property access while maintaining immutability: - -### Reading Properties - -```php -$user = UserDataObject::make($userData); - -// Both syntaxes work for reading -$user->name; // Object property access -$user['name']; // Array access - -// Check if property exists -if (isset($user['email'])) { - echo $user['email']; -} -``` - -### Array Access Immutability - -```php -$user = UserDataObject::make($userData); - -// Direct property assignment works (properties are mutable) -$user->name = 'Jane Doe'; // ✅ This works - -// Array access is read-only for immutability -$user['name'] = 'Jane Doe'; // ❌ LogicException -unset($user['name']); // ❌ LogicException -``` - -::: tip -For true immutability, declare your properties as `readonly`: - -```php -class ImmutableUserDataObject extends DataObject -{ - public function __construct( - public readonly string $name, - public readonly int $age, - public readonly string $email - ) {} -} -``` -::: - -## Serialization - -### Array Conversion - -Convert Data Objects to arrays for database storage or API responses: - -```php -$user = UserDataObject::make($userData, true); -$array = $user->toArray(); - -// Nested objects are also converted to arrays -print_r($array); -// [ -// 'name' => 'John Doe', -// 'address' => [ -// 'street' => '123 Main St', -// 'city' => 'Boston', -// 'zip_code' => '02101' -// ] -// ] -``` - -### JSON Serialization - -Data Objects are automatically JSON serializable: - -```php -$user = UserDataObject::make($userData, true); - -// Direct JSON encoding -$json = json_encode($user); - -// In HTTP responses -return response()->json($user); -``` - -## Advanced Features - -### Custom Property Conversion - -Override the default naming convention conversion methods: - -```php -class CustomDataObject extends DataObject -{ - public function __construct( - public string $snake_case_param, - public string $multi_word_parameter_name - ) {} - - /** - * Convert property name to data key format. - */ - public static function convertPropertyToDataKey(string $input): string - { - return Str::camel($input); // camelCase to camelCase - } - - /** - * Convert data key to property name format. - */ - public static function convertDataKeyToProperty(string $input): string - { - return Str::snake($input); // camelCase to snake_case - } -} - -$object = CustomDataObject::make([ - 'snakeCaseParam' => 'foo', // Maps to snake_case_param - 'multiWordParameterName' => 'bar' // Maps to multi_word_parameter_name -]); -``` - -### Custom Dependencies Resolving - -You can customize how specific types are resolved when using `autoResolve = true` by overriding the `getCustomizedDependencies` method: - -```php -use Money\Money; -use Money\Currency; - -class OrderDataObject extends DataObject -{ - public function __construct( - public string $orderNumber, - public Money $totalAmount, - public CustomStatus $status - ) {} - - /** - * Define custom resolvers for specific types. - */ - protected static function getCustomizedDependencies(): array - { - return array_merge(parent::getCustomizedDependencies(), [ - // Custom Money object resolver - Money::class => function ($value) { - if ($value instanceof Money) { - return $value; - } - - // Expect array with 'amount' and 'currency' - if (is_array($value)) { - return new Money( - $value['amount'] ?? 0, - new Currency($value['currency'] ?? 'USD') - ); - } - - return new Money(0, new Currency('USD')); - }, - - // Custom status enum resolver - CustomStatus::class => function ($value) { - if ($value instanceof CustomStatus) { - return $value; - } - - return CustomStatus::from($value); - }, - ]); - } -} - -$order = OrderDataObject::make([ - 'order_number' => 'ORD-12345', - 'total_amount' => [ - 'amount' => 2999, // $29.99 in cents - 'currency' => 'USD' - ], - 'status' => 'pending' -], true); - -$order->totalAmount->getAmount(); // 2999 -$order->status->value; // 'pending' -``` - -The `getCustomizedDependencies` method returns an array where: - -- **Keys** are the fully qualified class names or interface names -- **Values** are callable functions that handle the conversion logic - -Built-in dependencies include: -- `DateTimeInterface::class` -- `CarbonInterface::class` -- `DateTime::class` -- `Carbon::class` - -::: tip -Custom dependency resolvers are called recursively, so they work with deeply nested Data Objects. Always merge with `parent::getCustomizedDependencies()` to preserve built-in DateTime handling. -::: - -### Custom Serializer - -You can customize how Data Objects are serialized by overriding the `getSerializers` method. This allows you to control the output format and transform specific properties during serialization: - -```php -class ProductDataObject extends DataObject -{ - public function __construct( - public string $name, - public Money $price, - public DateTime $createdAt, - public array $tags = [] - ) {} - - /** - * Define custom serializers for specific properties. - */ - protected function getSerializers(): array - { - return array_merge(parent::getSerializers(), [ - // Serialize Money objects as formatted currency - 'price' => function ($value) { - if ($value instanceof Money) { - return [ - 'amount' => $value->getAmount(), - 'currency' => $value->getCurrency()->getCode(), - 'formatted' => '$' . number_format($value->getAmount() / 100, 2) - ]; - } - return $value; - }, - - // Format DateTime consistently - 'createdAt' => function ($value) { - if ($value instanceof DateTime) { - return $value->format('c'); // ISO 8601 format - } - return $value; - }, - - // Transform tags array - 'tags' => function ($value) { - if (is_array($value)) { - return array_map('strtolower', $value); - } - return $value; - } - ]); - } -} - -$product = ProductDataObject::make([ - 'name' => 'Laptop', - 'price' => new Money(99999, new Currency('USD')), // $999.99 - 'created_at' => new DateTime('2023-12-25 10:00:00'), - 'tags' => ['ELECTRONICS', 'COMPUTERS', 'WORK'] -], true); - -$serialized = $product->toArray(); -// [ -// 'name' => 'Laptop', -// 'price' => [ -// 'amount' => 99999, -// 'currency' => 'USD', -// 'formatted' => '$999.99' -// ], -// 'created_at' => '2023-12-25T10:00:00+00:00', -// 'tags' => ['electronics', 'computers', 'work'] -// ] -``` - -The `getSerializers` method returns an array where: - -- **Keys** are property names (in camelCase format) -- **Values** are callable functions that transform the property value during serialization - -Built-in serializers include: -- `CarbonInterface::class` -- `Carbon::class` - -> Carbon values will be serialized to ISO 8601 format - -::: tip -Custom serializers are applied during `toArray()` and `jsonSerialize()` operations, allowing you to control exactly how your Data Objects appear in API responses and database storage. -::: - -### Default Values and Nullable Properties - -```php -class UserDataObject extends DataObject -{ - public function __construct( - public string $name, - public string $status = 'active', // Default value - public ?string $nickname = null, // Nullable - public array $preferences = [] // Default array - ) {} -} - -$user = UserDataObject::make([ - 'name' => 'John Doe' - // Other properties will use defaults -]); - -$user->status; // 'active' -$user->nickname; // null -count($user->preferences); // 0 -``` - -### Refreshing Data Objects - -Once you change the property values in data objects, you need to clear internal caches manually to prevent getting old results: - -```php -$user = UserDataObject::make([ - 'name' => 'Original Name', - 'age' => 30, -]); - -$user->toArray(); // This will cache the result - -// Modify properties -$user->name = 'Updated Name'; -$user->age = 35; - -// Refresh internal caches -$user->refresh(); -``` - -### Validation and Error Handling - -Data Objects throw exceptions for missing required properties: - -```php -try { - $user = UserDataObject::make([ - // Missing required 'name' property - 'age' => 30 - ]); -} catch (RuntimeException $e) { - echo $e->getMessage(); - // "Missing required property `name` in `App\DataObjects\UserDataObject`" -} -``` - -### Eloquent Model Integration - -Use the `AsDataObject` cast to automatically convert JSON database columns to Data Objects in your Eloquent models: - -```php -use Hypervel\Database\Eloquent\Casts\AsDataObject; -use Hypervel\Database\Eloquent\Model; - -class User extends Model -{ - /** - * Get the attributes that should be cast. - */ - protected function casts(): array - { - return [ - 'profile' => AsDataObject::castUsing(UserProfileDataObject::class), - 'settings' => AsDataObject::castUsing(UserSettingsDataObject::class), - 'address' => AsDataObject::castUsing(AddressDataObject::class), - ]; - } -} -``` - -**Using the cast in practice:** - -```php -// Create a user with Data Object attributes -$user = User::create([ - 'name' => 'John Doe', - 'email' => 'john@example.com', - 'profile' => [ - 'first_name' => 'John', - 'last_name' => 'Doe', - 'bio' => 'Software Developer', - 'avatar' => 'avatar.jpg' - ], - 'settings' => [ - 'email_notifications' => true, - 'sms_notifications' => false, - 'timezone' => 'America/New_York', - 'theme' => 'dark' - ] -]); - -// Access as Data Objects with full type safety -$user->profile->firstName; // 'John' -$user->profile->lastName; // 'Doe' -$user->settings->emailNotifications; // true -$user->settings->timezone; // 'America/New_York' - -// Modify Data Object properties -$user->settings->theme = 'light'; -$user->profile->bio = 'Senior Software Developer'; -$user->save(); - -// Database storage -// The 'profile' column stores: {"first_name":"John","last_name":"Doe","bio":"Senior Software Developer","avatar":"avatar.jpg"} -// The 'settings' column stores: {"email_notifications":true,"sms_notifications":false,"timezone":"America/New_York","theme":"light"} -``` - -**Migration for JSON columns:** - -```php -Schema::create('users', function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->string('email')->unique(); - $table->json('profile')->nullable(); - $table->json('settings')->nullable(); - $table->json('address')->nullable(); - $table->timestamps(); -}); -``` - -**Benefits of using `AsDataObject` cast:** - -- **Automatic serialization/deserialization**: Arrays are automatically converted to/from Data Objects -- **Type safety**: Full IDE support and type checking for nested data -- **AutoResolve enabled**: Nested Data Objects and DateTime instances are automatically resolved -- **Seamless ORM integration**: Works transparently with Eloquent's attribute system - -::: tip -The `AsDataObject` cast automatically enables `autoResolve = true`, so nested Data Objects and DateTime instances are automatically resolved from the JSON data. -::: - -## Best Practices - -### Use Type Declarations - -Always use proper type declarations for better type safety and IDE support: - -```php -// ✅ Good - explicit types -public function __construct( - public string $name, - public int $age, - public ?DateTime $birthDate = null -) {} - -// ❌ Avoid - no type declarations -public function __construct( - public $name, - public $age, - public $birthDate = null -) {} -``` - -### Organize Related Data Objects - -Group related Data Objects in dedicated directories: - -``` -app/ -├── DataObjects/ -│ ├── User/ -│ │ ├── UserDataObject.php -│ │ ├── AddressDataObject.php -│ │ └── PreferencesDataObject.php -│ └── Product/ -│ ├── ProductDataObject.php -│ └── CategoryDataObject.php -``` - -### Use AutoResolve Judiciously - -Enable `autoResolve` only when you need nested object resolution: - -```php -// For simple data transfer -$user = UserDataObject::make($data); // autoResolve = false - -// For complex nested structures -$user = UserDataObject::make($data, true); // autoResolve = true -``` - -### Handle DateTime Formats Consistently - -Define a consistent DateTime format across your application: - -```php -class BaseDataObject extends DataObject -{ - protected string $dateFormat = 'Y-m-d H:i:s'; -} - -// Extend from BaseDataObject for consistency -class UserDataObject extends BaseDataObject -{ - // ... -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/database-testing.md b/_archive/docs/0.3/docs/database-testing.md deleted file mode 100644 index 666d62005f..0000000000 --- a/_archive/docs/0.3/docs/database-testing.md +++ /dev/null @@ -1,213 +0,0 @@ -# Database Testing -[[toc]] - -## Introduction - -Hypervel provides a variety of helpful tools and assertions to make it easier to test your database driven applications. In addition, Hypervel model factories and seeders make it painless to create test database records using your application's Eloquent models and relationships. We'll discuss all of these powerful features in the following documentation. - -### Resetting the Database After Each Test - -Before proceeding much further, let's discuss how to reset your database after each of your tests so that data from a previous test does not interfere with subsequent tests. Hypervel's included `Hypervel\Foundation\Testing\RefreshDatabase` trait will take care of this for you. Simply use the trait on your test class: - -```php -get('/'); - - // ... - } -} -``` - -The `Hypervel\Foundation\Testing\RefreshDatabase` trait does not migrate your database if your schema is up to date. Instead, it will only execute the test within a database transaction. Therefore, any records added to the database by test cases that do not use this trait may still exist in the database. - -If you would like to totally reset the database, you may use the `Hypervel\Foundation\Testing\DatabaseMigrations` or `Hypervel\Foundation\Testing\DatabaseTruncation` traits instead. However, both of these options are significantly slower than the `RefreshDatabase` trait. - -## Model Factories - -When testing, you may need to insert a few records into your database before executing your test. Instead of manually specifying the value of each column when you create this test data, Hypervel allows you to define a set of default attributes for each of your [Eloquent models](/docs/eloquent) using [model factories](/docs/eloquent-factories). - -To learn more about creating and utilizing model factories to create models, please consult the complete [model factory documentation](/docs/eloquent-factories). Once you have defined a model factory, you may utilize the factory within your test to create models: - -```php -use App\Models\User; - -public function test_models_can_be_instantiated(): void -{ - $user = User::factory()->create(); - - // ... -} -``` - -## Running Seeders - -If you would like to use [database seeders](/docs/seeding) to populate your database during a feature test, you may invoke the `seed` method. By default, the `seed` method will execute the `DatabaseSeeder`, which should execute all of your other seeders. Alternatively, you pass a specific seeder class name to the `seed` method: - -```php -seed(); - - // Run a specific seeder... - $this->seed(OrderStatusSeeder::class); - - // ... - - // Run an array of specific seeders... - $this->seed([ - OrderStatusSeeder::class, - TransactionStatusSeeder::class, - // ... - ]); - } -} -``` - -Alternatively, you may instruct Hypervel to automatically seed the database before each test that uses the `RefreshDatabase` trait. You may accomplish this by defining a `$seed` property on your base test class: - -```php -assertDatabaseCount('users', 5); -``` - -#### assertDatabaseHas - -Assert that a table in the database contains records matching the given key / value query constraints: - -```php -$this->assertDatabaseHas('users', [ - 'email' => 'sally@example.com', -]); -``` - -#### assertDatabaseMissing - -Assert that a table in the database does not contain records matching the given key / value query constraints: - -```php -$this->assertDatabaseMissing('users', [ - 'email' => 'sally@example.com', -]); -``` - -#### assertSoftDeleted - -The `assertSoftDeleted` method may be used to assert a given Eloquent model has been "soft deleted": - -```php -$this->assertSoftDeleted($user); -``` - -#### assertNotSoftDeleted - -The `assertNotSoftDeleted` method may be used to assert a given Eloquent model hasn't been "soft deleted": - -```php -$this->assertNotSoftDeleted($user); -``` - -#### assertModelExists - -Assert that a given model exists in the database: - -```php -use App\Models\User; - -$user = User::factory()->create(); - -$this->assertModelExists($user); -``` - -#### assertModelMissing - -Assert that a given model does not exist in the database: - -```php -use App\Models\User; - -$user = User::factory()->create(); - -$user->delete(); - -$this->assertModelMissing($user); -``` - -#### expectsDatabaseQueryCount - -The `expectsDatabaseQueryCount` method may be invoked at the beginning of your test to specify the total number of database queries that you expect to be run during the test. If the actual number of executed queries does not exactly match this expectation, the test will fail: - -```php -$this->expectsDatabaseQueryCount(5); - -// Test... -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/database.md b/_archive/docs/0.3/docs/database.md deleted file mode 100644 index 3a1fbdc595..0000000000 --- a/_archive/docs/0.3/docs/database.md +++ /dev/null @@ -1,357 +0,0 @@ -# Database: Getting Started -[[toc]] - -## Introduction - -Almost every modern web application interacts with a database. Hypervel makes interacting with databases extremely simple across a variety of supported databases using raw SQL, a [fluent query builder](/docs/queries), and the [Eloquent ORM](/docs/eloquent). Currently, Hypervel provides first-party support for five databases: - -
    - -- MariaDB 10.3+ ([Version Policy](https://mariadb.org/about/#maintenance-policy)) -- MySQL 5.7+ ([Version Policy](https://en.wikipedia.org/wiki/MySQL#Release_history)) -- PostgreSQL 10.0+ ([Version Policy](https://www.postgresql.org/support/versioning/)) -- SQLite 3.26.0+ -- SQL Server 2017+ ([Version Policy](https://docs.microsoft.com/en-us/lifecycle/products/?products=sql-server)) - -
    - -::: info -By default Hypervel Hyperf doesn't include drivers for PostgresSQL and SQL Server. You can install these drivers with `hyperf/database-pgsql` and `hyperf/database-sqlserver-incubator` manually. -::: - -::: warning -Because MongoDB extension doesn't support coroutine feature, there's no mongo driver in Hypervel Hyperf at this moment. -::: - -### Configuration - -The configuration for Hypervel Hyperf's database services is located in your application's `config/database.php` configuration file. In this file, you may define all of your database connections, as well as specify which connection should be used by default. Most of the configuration options within this file are driven by the values of your application's environment variables. Examples for most of Hypervel Hyperf's supported database systems are provided in this file. - -#### SQLite Configuration - -SQLite databases are contained within a single file on your filesystem. You can create a new SQLite database using the `touch` command in your terminal: `touch database/database.sqlite`. After the database has been created, you may easily configure your environment variables to point to this database by placing the absolute path to the database in the `DB_DATABASE` environment variable: - -```ini -DB_CONNECTION=sqlite -DB_DATABASE=/absolute/path/to/database.sqlite -``` - -By default, foreign key constraints are enabled for SQLite connections. If you would like to disable them, you should set the `DB_FOREIGN_KEYS` environment variable to `false`: - -```ini -DB_FOREIGN_KEYS=false -``` - -::: note -Hypervel Hyperf will automatically create a `database/database.sqlite` file and run the default [database migrations](/docs/migrations) for you when you install from composer. -::: - -### Read and Write Connections - -Sometimes you may wish to use one database connection for SELECT statements, and another for INSERT, UPDATE, and DELETE statements. Hypervel makes this a breeze, and the proper connections will always be used whether you are using raw queries, the query builder, or the Eloquent ORM. - -To see how read / write connections should be configured, let's look at this example: - -```php -'mysql' => [ - 'read' => [ - 'host' => [ - '192.168.1.1', - '196.168.1.2', - ], - ], - 'write' => [ - 'host' => [ - '196.168.1.3', - ], - ], - 'sticky' => true, - - 'database' => env('DB_DATABASE', 'laravel'), - 'username' => env('DB_USERNAME', 'root'), - 'password' => env('DB_PASSWORD', ''), - 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => env('DB_CHARSET', 'utf8mb4'), - 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), - 'prefix' => '', - 'prefix_indexes' => true, - 'strict' => true, - 'engine' => null, -], -``` - -Note that three keys have been added to the configuration array: `read`, `write` and `sticky`. The `read` and `write` keys have array values containing a single key: `host`. The rest of the database options for the `read` and `write` connections will be merged from the main `mysql` configuration array. - -You only need to place items in the `read` and `write` arrays if you wish to override the values from the main `mysql` array. So, in this case, `192.168.1.1` will be used as the host for the "read" connection, while `192.168.1.3` will be used for the "write" connection. The database credentials, prefix, character set, and all other options in the main `mysql` array will be shared across both connections. When multiple values exist in the `host` configuration array, a database host will be randomly chosen for each request. - -#### The `sticky` Option - -The `sticky` option is an *optional* value that can be used to allow the immediate reading of records that have been written to the database during the current request cycle. If the `sticky` option is enabled and a "write" operation has been performed against the database during the current request cycle, any further "read" operations will use the "write" connection. This ensures that any data written during the request cycle can be immediately read back from the database during that same request. It is up to you to decide if this is the desired behavior for your application. - -### Connection Pooling - -Hypervel implements database connection pooling to efficiently manage and reuse database connections across coroutines. Connection pooling helps improve performance by reducing the overhead of establishing new database connections for each request. - -```php -'mysql' => [ - 'pool' => [ - 'min_connections' => 1, - 'max_connections' => 10, - 'connect_timeout' => 10.0, - 'wait_timeout' => 3.0, - 'heartbeat' => -1, - 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), - ] -], -``` - -Each configuration option serves a specific purpose: - -- `min_connections`: The minimum number of connections that will be created and maintained in the pool, ensuring a base level of available connections. -- `max_connections`: The maximum number of connections that can be created. When all connections are in use and a new request arrives, it will wait for an available connection. -- `connect_timeout`: The maximum time to wait while trying to establish a new database connection. -- `wait_timeout`: The maximum time to wait for an available connection from the pool before throwing an exception. -- `heartbeat`: The interval at which the pool checks if connections are still alive. -- `max_idle_time`: The maximum time a connection can remain idle in the pool before being closed. - -The connection pool automatically handles connection lifecycle management: - -1. When a connection is requested, the pool first tries to reuse an existing idle connection -2. If no idle connections are available and the pool hasn't reached `max_connections`, a new connection is created -3. If the pool is at capacity, the request waits up to `wait_timeout` seconds for a connection to become available -4. After use, connections are returned to the pool instead of being closed, allowing for reuse - -This approach significantly reduces the overhead of database operations in high-concurrency scenarios by eliminating the need to establish new connections for each request. - -## Running SQL Queries - -Once you have configured your database connection, you may run queries using the `DB` facade. The `DB` facade provides methods for each type of query: `select`, `update`, `insert`, `delete`, and `statement`. - -#### Running a Select Query - -To run a basic SELECT query, you may use the `select` method on the `DB` facade: - -```php - $users]); - } -} -``` - -The first argument passed to the `select` method is the SQL query, while the second argument is any parameter bindings that need to be bound to the query. Typically, these are the values of the `where` clause constraints. Parameter binding provides protection against SQL injection. - -The `select` method will always return an `array` of results. Each result within the array will be a PHP `stdClass` object representing a record from the database: - -```php -use Hypervel\Support\Facades\DB; - -$users = DB::select('select * from users'); - -foreach ($users as $user) { - echo $user->name; -} -``` - -#### Selecting Scalar Values - -Sometimes your database query may result in a single, scalar value. Instead of being required to retrieve the query's scalar result from a record object, Hypervel allows you to retrieve this value directly using the `scalar` method: - -```php -$burgers = DB::scalar( - "select count(case when food = 'burger' then 1 end) as burgers from menu" -); -``` - -#### Using Named Bindings - -Instead of using `?` to represent your parameter bindings, you may execute a query using named bindings: - -```php -$results = DB::select('select * from users where id = :id', ['id' => 1]); -``` - -#### Running an Insert Statement - -To execute an `insert` statement, you may use the `insert` method on the `DB` facade. Like `select`, this method accepts the SQL query as its first argument and bindings as its second argument: - -```php -use Hypervel\Support\Facades\DB; - -DB::insert('insert into users (id, name) values (?, ?)', [1, 'Marc']); -``` - -#### Running an Update Statement - -The `update` method should be used to update existing records in the database. The number of rows affected by the statement is returned by the method: - -```php -use Hypervel\Support\Facades\DB; - -$affected = DB::update( - 'update users set votes = 100 where name = ?', - ['Anita'] -); -``` - -#### Running a Delete Statement - -The `delete` method should be used to delete records from the database. Like `update`, the number of rows affected will be returned by the method: - -```php -use Hypervel\Support\Facades\DB; - -$deleted = DB::delete('delete from users'); -``` - -#### Running a General Statement - -Some database statements do not return any value. For these types of operations, you may use the `statement` method on the `DB` facade: - -```php -DB::statement('drop table users'); -``` - -#### Running an Unprepared Statement - -Sometimes you may want to execute an SQL statement without binding any values. You may use the `DB` facade's `unprepared` method to accomplish this: - -```php -DB::unprepared('update users set votes = 100 where name = "Dries"'); -``` - -::: warning -Since unprepared statements do not bind parameters, they may be vulnerable to SQL injection. You should never allow user controlled values within an unprepared statement. -::: - -#### Implicit Commits - -When using the `DB` facade's `statement` and `unprepared` methods within transactions you must be careful to avoid statements that cause [implicit commits](https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html). These statements will cause the database engine to indirectly commit the entire transaction, leaving Hypervel unaware of the database's transaction level. An example of such a statement is creating a database table: - -```php -DB::unprepared('create table a (col varchar(1) null)'); -``` - -Please refer to the MySQL manual for [a list of all statements](https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html) that trigger implicit commits. - -### Using Multiple Database Connections - -If your application defines multiple connections in your `config/database.php` configuration file, you may access each connection via the `connection` method provided by the `DB` facade. The connection name passed to the `connection` method should correspond to one of the connections listed in your `config/database.php` configuration file or configured at runtime using the `config` helper: - -```php -use Hypervel\Support\Facades\DB; - -$users = DB::connection('sqlite')->select(/* ... */); -``` - -You may access the raw, underlying PDO instance of a connection using the `getPdo` method on a connection instance: - -```php -$pdo = DB::connection()->getPdo(); -``` - -### Listening for Query Events - -If you would like to specify a closure that is invoked for each SQL query executed by your application, you may use the `DB` facade's `listen` method. This method can be useful for logging queries or debugging. You may register your query listener closure in the `boot` method of a [service provider](/docs/providers): - -```php -sql; - // $query->bindings; - // $query->time; - }); - } -} -``` - -## Database Transactions - -You may use the `transaction` method provided by the `DB` facade to run a set of operations within a database transaction. If an exception is thrown within the transaction closure, the transaction will automatically be rolled back and the exception is re-thrown. If the closure executes successfully, the transaction will automatically be committed. You don't need to worry about manually rolling back or committing while using the `transaction` method: - -```php -use Hypervel\Support\Facades\DB; - -DB::transaction(function () { - DB::update('update users set votes = 1'); - - DB::delete('delete from posts'); -}); -``` - -#### Handling Deadlocks - -The `transaction` method accepts an optional second argument which defines the number of times a transaction should be retried when a deadlock occurs. Once these attempts have been exhausted, an exception will be thrown: - -```php -use Hypervel\Support\Facades\DB; - -DB::transaction(function () { - DB::update('update users set votes = 1'); - - DB::delete('delete from posts'); -}, 5); -``` - -#### Manually Using Transactions - -If you would like to begin a transaction manually and have complete control over rollbacks and commits, you may use the `beginTransaction` method provided by the `DB` facade: - -```php -use Hypervel\Support\Facades\DB; - -DB::beginTransaction(); -``` - -You can rollback the transaction via the `rollBack` method: - -```php -DB::rollBack(); -``` - -Lastly, you can commit a transaction via the `commit` method: - -```php -DB::commit(); -``` - -::: note -The `DB` facade's transaction methods control the transactions for both the [query builder](/docs/queries) and [Eloquent ORM](/docs/eloquent). -::: diff --git a/_archive/docs/0.3/docs/deployment.md b/_archive/docs/0.3/docs/deployment.md deleted file mode 100644 index 931e2b6e36..0000000000 --- a/_archive/docs/0.3/docs/deployment.md +++ /dev/null @@ -1,80 +0,0 @@ -# Deployment -[[toc]] - -## Introduction - -When you're ready to deploy your Hypervel application to production, there are some important things you can do to make sure your application is running as efficiently as possible. In this document, we'll cover some great starting points for making sure your Hypervel application is deployed properly. - -## Server Requirements - -Hypervel has a few system requirements. You should ensure that your web server has the following minimum PHP version and extensions: - - - PHP >= 8.2 - - [Swoole PHP extension](https://github.com/swoole/swoole-src) >= 5.1 - - JSON PHP extension - - PDO PHP extension - - Pcntl PHP extension - - Session extension (If you need to use session) - - OpenSSL PHP extension (If you need to use the HTTPS) - - Redis PHP extension (If you need to use the Redis Client) - -### Docker - -If you plan to deploy your application via docker, you can find various Dockerfiles in the [hyperf/hyperf-docker](https://github.com/hyperf/hyperf-docker) project, or use a pre-built image based on [hyperf/hyperf](https://hub.docker.com/r/hyperf/hyperf). - -## Server Configuration - -### Nginx for HTTP - -If you are deploying your application to a server that is running Nginx, you may use the following configuration file as a starting point for configuring your web server. Most likely, this file will need to be customized depending on your server's configuration. - -```nginx -server { - # listening port - listen 80; - # Bound domain name, fill in your domain name - server_name proxy.hypervel.org; - - location / { - # Forward the client's Host and IP information to the corresponding node - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - # Forward cookies, set SameSite - proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; - - # Execute proxy access to real server - proxy_pass http://127.0.0.1:9501; - } -} -``` - -### Nginx for Websocket - -If you plan to proxy your websocket services, you may use the following configuration file. - -```nginx -server { - listen 80; - server_name websocket.hypervel.org; - - location / { - # WebSocket Header - proxy_http_version 1.1; - proxy_set_header Upgrade websocket; - proxy_set_header Connection "Upgrade"; - - # Forward the client's Host and IP information to the corresponding node - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - - # The connection between the client and the server is automatically disconnected after 60s of no interaction, please set according to the actual business scenario - proxy_read_timeout 60s ; - - # Execute proxy access to real server - proxy_pass http://127.0.0.1:9502; - } -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/eloquent-collections.md b/_archive/docs/0.3/docs/eloquent-collections.md deleted file mode 100644 index 2a73ae2054..0000000000 --- a/_archive/docs/0.3/docs/eloquent-collections.md +++ /dev/null @@ -1,251 +0,0 @@ -# Eloquent: Collections -[[toc]] - -## Introduction - -All Eloquent methods that return more than one model result will return instances of the `Hypervel\Database\Eloquent` class, including results retrieved via the `get` method or accessed via a relationship. The Eloquent collection object extends Hypervel's [base collection](/docs/collections), so it naturally inherits dozens of methods used to fluently work with the underlying array of Eloquent models. Be sure to review the Hypervel collection documentation to learn all about these helpful methods! - -All collections also serve as iterators, allowing you to loop over them as if they were simple PHP arrays: - -```php -use App\Models\User; - -$users = User::where('active', 1)->get(); - -foreach ($users as $user) { - echo $user->name; -} -``` - -However, as previously mentioned, collections are much more powerful than arrays and expose a variety of map / reduce operations that may be chained using an intuitive interface. For example, we may remove all inactive models and then gather the first name for each remaining user: - -```php -$names = User::all()->reject(function ($user) { - return $user->active === false; -})->map(function ($user) { - return $user->name; -}); -``` - -#### Eloquent Collection Conversion - -While most Eloquent collection methods return a new instance of an Eloquent collection, the `collapse`, `flatten`, `flip`, `keys`, `pluck`, and `zip` methods return a [base collection](/docs/collections) instance. Likewise, if a `map` operation returns a collection that does not contain any Eloquent models, it will be converted to a base collection instance. - -## Available Methods - -All Eloquent collections extend the base [Hypervel collection](/docs/collections#available-methods) object; therefore, they inherit all of the powerful methods provided by the base collection class. - -In addition, the `Hypervel\Database\Eloquent` class provides a superset of methods to aid with managing your model collections. Most methods return `Hypervel\Database\Eloquent` instances; however, some methods, like `modelKeys`, return an `Hypervel\Support\Collection` instance. - - - -
    - -[contains](#method-contains) -[diff](#method-diff) -[except](#method-except) -[find](#method-find) -[fresh](#method-fresh) -[intersect](#method-intersect) -[load](#method-load) -[loadMissing](#method-loadMissing) -[modelKeys](#method-modelKeys) -[makeVisible](#method-makeVisible) -[makeHidden](#method-makeHidden) -[only](#method-only) -[toQuery](#method-toquery) -[unique](#method-unique) - -
    - - -#### contains($key, $operator = null, $value = null) - -The `contains` method may be used to determine if a given model instance is contained by the collection. This method accepts a primary key or a model instance: - -```php -$users->contains(1); - -$users->contains(User::find(1)); -``` - - -#### diff($items) - -The `diff` method returns all of the models that are not present in the given collection: - -```php -use App\Models\User; - -$users = $users->diff(User::whereIn('id', [1, 2, 3])->get()); -``` - - -#### except($keys) - -The `except` method returns all of the models that do not have the given primary keys: - -```php -$users = $users->except([1, 2, 3]); -``` - - -#### find($key) - -The `find` method returns the model that has a primary key matching the given key. If `$key` is a model instance, `find` will attempt to return a model matching the primary key. If `$key` is an array of keys, `find` will return all models which have a primary key in the given array: - -```php -$users = User::all(); - -$user = $users->find(1); -``` - - -#### fresh($with = []) - -The `fresh` method retrieves a fresh instance of each model in the collection from the database. In addition, any specified relationships will be eager loaded: - -```php -$users = $users->fresh(); - -$users = $users->fresh('comments'); -``` - - -#### intersect($items) - -The `intersect` method returns all of the models that are also present in the given collection: - -```php -use App\Models\User; - -$users = $users->intersect(User::whereIn('id', [1, 2, 3])->get()); -``` - - -#### load($relations) - -The `load` method eager loads the given relationships for all models in the collection: - -```php -$users->load(['comments', 'posts']); - -$users->load('comments.author'); -``` - - -#### loadMissing($relations) - -The `loadMissing` method eager loads the given relationships for all models in the collection if the relationships are not already loaded: - -```php -$users->loadMissing(['comments', 'posts']); - -$users->loadMissing('comments.author'); -``` - - -#### modelKeys() - -The `modelKeys` method returns the primary keys for all models in the collection: - -```php -$users->modelKeys(); - -// [1, 2, 3, 4, 5] -``` - - -#### makeVisible($attributes) - -The `makeVisible` method [makes attributes visible](/docs/eloquent-serialization#hiding-attributes-from-json) that are typically "hidden" on each model in the collection: - -```php -$users = $users->makeVisible(['address', 'phone_number']); -``` - - -#### makeHidden($attributes) - -The `makeHidden` method [hides attributes](/docs/eloquent-serialization#hiding-attributes-from-json) that are typically "visible" on each model in the collection: - -```php -$users = $users->makeHidden(['address', 'phone_number']); -``` - - -#### only($keys) - -The `only` method returns all of the models that have the given primary keys: - -```php -$users = $users->only([1, 2, 3]); -``` - - -#### toQuery() - -The `toQuery` method returns an Eloquent query builder instance containing a `whereIn` constraint on the collection model's primary keys: - -```php -use App\Models\User; - -$users = User::where('status', 'VIP')->get(); - -$users->toQuery()->update([ - 'status' => 'Administrator', -]); -``` - - -#### unique($key = null, $strict = false) - -The `unique` method returns all of the unique models in the collection. Any models of the same type with the same primary key as another model in the collection are removed: - -```php -$users = $users->unique(); -``` - - -## Custom Collections - -If you would like to use a custom `Collection` object when interacting with a given model, you may define a `newCollection` method on your model: - -```php - - */ -class UserFactory extends Factory -{ - /** - * The current password being used by the factory. - */ - protected static ?string $password; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - 'name' => fake()->name(), - 'email' => fake()->unique()->safeEmail(), - 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), - 'remember_token' => Str::random(10), - ]; - } - - /** - * Indicate that the model's email address should be unverified. - */ - public function unverified(): static - { - return $this->state(fn (array $attributes) => [ - 'email_verified_at' => null, - ]); - } -} -``` - -As you can see, in their most basic form, factories are classes that extend Hypervel's base factory class and define a `definition` method. The `definition` method returns the default set of attribute values that should be applied when creating a model using the factory. - -Via the `fake` helper, factories have access to the [Faker](https://github.com/FakerPHP/Faker) PHP library, which allows you to conveniently generate various kinds of random data for testing and seeding. - -::: note -You can change your application's Faker locale by updating the `faker_locale` option in your `config/app.php` configuration file. -::: - -## Defining Model Factories - -### Generating Factories - -To create a factory, execute the `make:factory` [Artisan command](/docs/artisan): - -```shell -php artisan make:factory PostFactory -``` - -The new factory class will be placed in your `database/factories` directory. - -#### Model and Factory Discovery Conventions - -Once you have defined your factories, you may use the static `factory` method provided to your models by the `Hypervel\Database\Eloquent\Factories\HasFactory` trait in order to instantiate a factory instance for that model. - -The `HasFactory` trait's `factory` method will use conventions to determine the proper factory for the model the trait is assigned to. Specifically, the method will look for a factory in the `Database\Factories` namespace that has a class name matching the model name and is suffixed with `Factory`. If these conventions do not apply to your particular application or factory, you may overwrite the `newFactory` method on your model to return an instance of the model's corresponding factory directly: - -```php -use Database\Factories\Administration\FlightFactory; - -/** - * Create a new factory instance for the model. - */ -protected static function newFactory() -{ - return FlightFactory::new(); -} -``` - -Then, define a `model` property on the corresponding factory: - -```php -use App\Administration\Flight; -use Hypervel\Database\Eloquent\Factories\Factory; - -class FlightFactory extends Factory -{ - /** - * The name of the factory's corresponding model. - * - * @var class-string<\Hypervel\Database\Eloquent\Model> - */ - protected $model = Flight::class; -} -``` - -### Factory States - -State manipulation methods allow you to define discrete modifications that can be applied to your model factories in any combination. For example, your `Database\Factories\UserFactory` factory might contain a `suspended` state method that modifies one of its default attribute values. - -State transformation methods typically call the `state` method provided by Hypervel's base factory class. The `state` method accepts a closure which will receive the array of raw attributes defined for the factory and should return an array of attributes to modify: - -```php -use Hypervel\Database\Eloquent\Factories\Factory; - -/** - * Indicate that the user is suspended. - */ -public function suspended(): Factory -{ - return $this->state(function (array $attributes) { - return [ - 'account_status' => 'suspended', - ]; - }); -} -``` - -#### "Trashed" State - -If your Eloquent model can be [soft deleted](/docs/eloquent#soft-deleting), you may invoke the built-in `trashed` state method to indicate that the created model should already be "soft deleted". You do not need to manually define the `trashed` state as it is automatically available to all factories: - -```php -use App\Models\User; - -$user = User::factory()->trashed()->create(); -``` - -### Factory Callbacks - -Factory callbacks are registered using the `afterMaking` and `afterCreating` methods and allow you to perform additional tasks after making or creating a model. You should register these callbacks by defining a `configure` method on your factory class. This method will be automatically called by Hypervel when the factory is instantiated: - -```php -namespace Database\Factories; - -use App\Models\User; -use Hypervel\Database\Eloquent\Factories\Factory; - -class UserFactory extends Factory -{ - /** - * Configure the model factory. - */ - public function configure(): static - { - return $this->afterMaking(function (User $user) { - // ... - })->afterCreating(function (User $user) { - // ... - }); - } - - // ... -} -``` - -You may also register factory callbacks within state methods to perform additional tasks that are specific to a given state: - -```php -use App\Models\User; -use Hypervel\Database\Eloquent\Factories\Factory; - -/** - * Indicate that the user is suspended. - */ -public function suspended(): Factory -{ - return $this->state(function (array $attributes) { - return [ - 'account_status' => 'suspended', - ]; - })->afterMaking(function (User $user) { - // ... - })->afterCreating(function (User $user) { - // ... - }); -} -``` - -## Creating Models Using Factories - -### Instantiating Models - -Once you have defined your factories, you may use the static `factory` method provided to your models by the `Hypervel\Database\Eloquent\Factories\HasFactory` trait in order to instantiate a factory instance for that model. Let's take a look at a few examples of creating models. First, we'll use the `make` method to create models without persisting them to the database: - -```php -use App\Models\User; - -$user = User::factory()->make(); -``` - -You may create a collection of many models using the `count` method: - -```php -$users = User::factory()->count(3)->make(); -``` - -#### Applying States - -You may also apply any of your [states](#factory-states) to the models. If you would like to apply multiple state transformations to the models, you may simply call the state transformation methods directly: - -```php -$users = User::factory()->count(5)->suspended()->make(); -``` - -#### Overriding Attributes - -If you would like to override some of the default values of your models, you may pass an array of values to the `make` method. Only the specified attributes will be replaced while the rest of the attributes remain set to their default values as specified by the factory: - -```php -$user = User::factory()->make([ - 'name' => 'Abigail Otwell', -]); -``` - -Alternatively, the `state` method may be called directly on the factory instance to perform an inline state transformation: - -```php -$user = User::factory()->state([ - 'name' => 'Abigail Otwell', -])->make(); -``` - -> [!NOTE] -> [Mass assignment protection](/docs/eloquent#mass-assignment) is automatically disabled when creating models using factories. - -### Persisting Models - -The `create` method instantiates model instances and persists them to the database using Eloquent's `save` method: - -```php -use App\Models\User; - -// Create a single App\Models\User instance... -$user = User::factory()->create(); - -// Create three App\Models\User instances... -$users = User::factory()->count(3)->create(); -``` - -You may override the factory's default model attributes by passing an array of attributes to the `create` method: - -```php -$user = User::factory()->create([ - 'name' => 'Abigail', -]); -``` - -### Sequences - -Sometimes you may wish to alternate the value of a given model attribute for each created model. You may accomplish this by defining a state transformation as a sequence. For example, you may wish to alternate the value of an `admin` column between `Y` and `N` for each created user: - -```php -use App\Models\User; -use Hypervel\Database\Eloquent\Factories\Sequence; - -$users = User::factory() - ->count(10) - ->state(new Sequence( - ['admin' => 'Y'], - ['admin' => 'N'], - )) - ->create(); -``` - -In this example, five users will be created with an `admin` value of `Y` and five users will be created with an `admin` value of `N`. - -If necessary, you may include a closure as a sequence value. The closure will be invoked each time the sequence needs a new value: - -```php -use Hypervel\Database\Eloquent\Factories\Sequence; - -$users = User::factory() - ->count(10) - ->state(new Sequence( - fn (Sequence $sequence) => ['role' => UserRoles::all()->random()], - )) - ->create(); -``` - -Within a sequence closure, you may access the `$index` or `$count` properties on the sequence instance that is injected into the closure. The `$index` property contains the number of iterations through the sequence that have occurred thus far, while the `$count` property contains the total number of times the sequence will be invoked: - -```php -$users = User::factory() - ->count(10) - ->sequence(fn (Sequence $sequence) => ['name' => 'Name '.$sequence->index]) - ->create(); -``` - -For convenience, sequences may also be applied using the `sequence` method, which simply invokes the `state` method internally. The `sequence` method accepts a closure or arrays of sequenced attributes: - -```php -$users = User::factory() - ->count(2) - ->sequence( - ['name' => 'First User'], - ['name' => 'Second User'], - ) - ->create(); -``` - -## Factory Relationships - -### Has Many Relationships - -Next, let's explore building Eloquent model relationships using Hypervel's fluent factory methods. First, let's assume our application has an `App\Models\User` model and an `App\Models\Post` model. Also, let's assume that the `User` model defines a `hasMany` relationship with `Post`. We can create a user that has three posts using the `has` method provided by the Hypervel's factories. The `has` method accepts a factory instance: - -```php -use App\Models\Post; -use App\Models\User; - -$user = User::factory() - ->has(Post::factory()->count(3)) - ->create(); -``` - -By convention, when passing a `Post` model to the `has` method, Hypervel will assume that the `User` model must have a `posts` method that defines the relationship. If necessary, you may explicitly specify the name of the relationship that you would like to manipulate: - -```php -$user = User::factory() - ->has(Post::factory()->count(3), 'posts') - ->create(); -``` - -Of course, you may perform state manipulations on the related models. In addition, you may pass a closure-based state transformation if your state change requires access to the parent model: - -```php -$user = User::factory() - ->has( - Post::factory() - ->count(3) - ->state(function (array $attributes, User $user) { - return ['user_type' => $user->type]; - }) - ) - ->create(); -``` - -#### Using Magic Methods - -For convenience, you may use Hypervel's magic factory relationship methods to build relationships. For example, the following example will use convention to determine that the related models should be created via a `posts` relationship method on the `User` model: - -```php -$user = User::factory() - ->hasPosts(3) - ->create(); -``` - -When using magic methods to create factory relationships, you may pass an array of attributes to override on the related models: - -```php -$user = User::factory() - ->hasPosts(3, [ - 'published' => false, - ]) - ->create(); -``` - -You may provide a closure-based state transformation if your state change requires access to the parent model: - -```php -$user = User::factory() - ->hasPosts(3, function (array $attributes, User $user) { - return ['user_type' => $user->type]; - }) - ->create(); -``` - -### Belongs To Relationships - -Now that we have explored how to build "has many" relationships using factories, let's explore the inverse of the relationship. The `for` method may be used to define the parent model that factory created models belong to. For example, we can create three `App\Models\Post` model instances that belong to a single user: - -```php -use App\Models\Post; -use App\Models\User; - -$posts = Post::factory() - ->count(3) - ->for(User::factory()->state([ - 'name' => 'Jessica Archer', - ])) - ->create(); -``` - -If you already have a parent model instance that should be associated with the models you are creating, you may pass the model instance to the `for` method: - -```php -$user = User::factory()->create(); - -$posts = Post::factory() - ->count(3) - ->for($user) - ->create(); -``` - -#### Using Magic Methods - -For convenience, you may use Hypervel's magic factory relationship methods to define "belongs to" relationships. For example, the following example will use convention to determine that the three posts should belong to the `user` relationship on the `Post` model: - -```php -$posts = Post::factory() - ->count(3) - ->forUser([ - 'name' => 'Jessica Archer', - ]) - ->create(); -``` - -### Many to Many Relationships - -Like [has many relationships](#has-many-relationships), "many to many" relationships may be created using the `has` method: - -```php -use App\Models\Role; -use App\Models\User; - -$user = User::factory() - ->has(Role::factory()->count(3)) - ->create(); -``` - -#### Pivot Table Attributes - -If you need to define attributes that should be set on the pivot / intermediate table linking the models, you may use the `hasAttached` method. This method accepts an array of pivot table attribute names and values as its second argument: - -```php -use App\Models\Role; -use App\Models\User; - -$user = User::factory() - ->hasAttached( - Role::factory()->count(3), - ['active' => true] - ) - ->create(); -``` - -You may provide a closure-based state transformation if your state change requires access to the related model: - -```php -$user = User::factory() - ->hasAttached( - Role::factory() - ->count(3) - ->state(function (array $attributes, User $user) { - return ['name' => $user->name.' Role']; - }), - ['active' => true] - ) - ->create(); -``` - -If you already have model instances that you would like to be attached to the models you are creating, you may pass the model instances to the `hasAttached` method. In this example, the same three roles will be attached to all three users: - -```php -$roles = Role::factory()->count(3)->create(); - -$user = User::factory() - ->count(3) - ->hasAttached($roles, ['active' => true]) - ->create(); -``` - -#### Using Magic Methods - -For convenience, you may use Hypervel's magic factory relationship methods to define many to many relationships. For example, the following example will use convention to determine that the related models should be created via a `roles` relationship method on the `User` model: - -```php -$user = User::factory() - ->hasRoles(1, [ - 'name' => 'Editor' - ]) - ->create(); -``` - -### Polymorphic Relationships - -[Polymorphic relationships](/docs/eloquent-relationships#polymorphic-relationships) may also be created using factories. Polymorphic "morph many" relationships are created in the same way as typical "has many" relationships. For example, if an `App\Models\Post` model has a `morphMany` relationship with an `App\Models\Comment` model: - -```php -use App\Models\Post; - -$post = Post::factory()->hasComments(3)->create(); -``` - -#### Morph To Relationships - -Magic methods may not be used to create `morphTo` relationships. Instead, the `for` method must be used directly and the name of the relationship must be explicitly provided. For example, imagine that the `Comment` model has a `commentable` method that defines a `morphTo` relationship. In this situation, we may create three comments that belong to a single post by using the `for` method directly: - -```php -$comments = Comment::factory()->count(3)->for( - Post::factory(), 'commentable' -)->create(); -``` - -#### Polymorphic Many to Many Relationships - -Polymorphic "many to many" (`morphToMany` / `morphedByMany`) relationships may be created just like non-polymorphic "many to many" relationships: - -```php -use App\Models\Tag; -use App\Models\Video; - -$videos = Video::factory() - ->hasAttached( - Tag::factory()->count(3), - ['public' => true] - ) - ->create(); -``` - -Of course, the magic `has` method may also be used to create polymorphic "many to many" relationships: - -```php -$videos = Video::factory() - ->hasTags(3, ['public' => true]) - ->create(); -``` - -### Defining Relationships Within Factories - -To define a relationship within your model factory, you will typically assign a new factory instance to the foreign key of the relationship. This is normally done for the "inverse" relationships such as `belongsTo` and `morphTo` relationships. For example, if you would like to create a new user when creating a post, you may do the following: - -```php -use App\Models\User; - -/** - * Define the model's default state. - * - * @return array - */ -public function definition(): array -{ - return [ - 'user_id' => User::factory(), - 'title' => fake()->title(), - 'content' => fake()->paragraph(), - ]; -} -``` - -If the relationship's columns depend on the factory that defines it you may assign a closure to an attribute. The closure will receive the factory's evaluated attribute array: - -```php -/** - * Define the model's default state. - * - * @return array - */ -public function definition(): array -{ - return [ - 'user_id' => User::factory(), - 'user_type' => function (array $attributes) { - return User::find($attributes['user_id'])->type; - }, - 'title' => fake()->title(), - 'content' => fake()->paragraph(), - ]; -} -``` - -### Recycling an Existing Model for Relationships - -If you have models that share a common relationship with another model, you may use the `recycle` method to ensure a single instance of the related model is recycled for all of the relationships created by the factory. - -For example, imagine you have `Airline`, `Flight`, and `Ticket` models, where the ticket belongs to an airline and a flight, and the flight also belongs to an airline. When creating tickets, you will probably want the same airline for both the ticket and the flight, so you may pass an airline instance to the `recycle` method: - -```php -Ticket::factory() - ->recycle(Airline::factory()->create()) - ->create(); -``` - -You may find the `recycle` method particularly useful if you have models belonging to a common user or team. - -The `recycle` method also accepts a collection of existing models. When a collection is provided to the `recycle` method, a random model from the collection will be chosen when the factory needs a model of that type: - -```php -Ticket::factory() - ->recycle($airlines) - ->create(); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/eloquent-mutators.md b/_archive/docs/0.3/docs/eloquent-mutators.md deleted file mode 100644 index 2241a4b628..0000000000 --- a/_archive/docs/0.3/docs/eloquent-mutators.md +++ /dev/null @@ -1,230 +0,0 @@ -# Eloquent: Mutators & Casting -[[toc]] - -## Introduction - -Accessors, mutators, and attribute casting allow you to transform Eloquent attribute values when you retrieve or set them on model instances. For example, you may want to use the [Hypervel encrypter](/docs/encryption) to encrypt a value while it is stored in the database, and then automatically decrypt the attribute when you access it on an Eloquent model. Or, you may want to convert a JSON string that is stored in your database to an array when it is accessed via your Eloquent model. - -## Accessors & Mutators - -### Defining An Accessor - -An accessor transforms an Eloquent attribute value when it is accessed. To define an accessor, create a `get{Attribute}Attribute` method on your model where `{Attribute}` is the "studly" cased name of the column you wish to access. - -In this example, we'll define an accessor for the `first_name` attribute. The accessor will automatically be called by Eloquent when attempting to retrieve the value of the `first_name` attribute: - -```php -first_name; -``` - -You are not limited to interacting with a single attribute within your accessor. You may also use accessors to return new, computed values from existing attributes: - -```php -/** - * Get the user's full name. - * - * @return string - */ -public function getFullNameAttribute() -{ - return "{$this->first_name} {$this->last_name}"; -} -``` - -::: tip -If you would like these computed values to be added to the array / JSON representations of your model, [you will need to append them](/docs/eloquent-serialization#appending-values-to-json). -::: - -### Defining A Mutator - -A mutator transforms an Eloquent attribute value when it is set. To define a mutator, define a `set{Attribute}Attribute` method on your model where `{Attribute}` is the "studly" cased name of the column you wish to access. - -Let's define a mutator for the `first_name` attribute. This mutator will be automatically called when we attempt to set the value of the `first_name` attribute on the model: - -```php -attributes['first_name'] = strtolower($value); - } -} -``` - -The mutator will receive the value that is being set on the attribute, allowing you to manipulate the value and set the manipulated value on the Eloquent model's internal `$attributes` property. To use our mutator, we only need to set the `first_name` attribute on an Eloquent model: - -```php -use App\Models\User; - -$user = User::find(1); - -$user->first_name = 'Sally'; -``` - -In this example, the `setFirstNameAttribute` function will be called with the value `Sally`. The mutator will then apply the `strtolower` function to the name and set its resulting value in the internal `$attributes` array. - -## Attribute Casting - -The `$casts` property on your model provides a convenient method of converting attributes to common data types. The `$casts` property should be an array where the key is the name of the attribute being cast and the value is the type you wish to cast the column to. The supported cast types are: `integer`, `real`, `float`, `double`, `decimal:`, `string`, `boolean`, `object`, `array`, `collection`, `date`, `datetime`, and `timestamp`. When casting to `decimal`, you must define the number of digits (`decimal:2`). - -To demonstrate attribute casting, let's cast the `is_admin` attribute, which is stored in our database as an integer (`0` or `1`) to a boolean value: - -```php - 'boolean', - ]; -} -``` - -After defining the cast, the `is_admin` attribute will always be cast to a boolean when you access it, even if the underlying value is stored in the database as an integer: - -```php -$user = App\Models\User::find(1); - -if ($user->is_admin) { - // -} -``` - -If you need to add a new, temporary cast at runtime, you may use the `mergeCasts` method. These cast definitions will be added to any of the casts already defined on the model: - -```php -$user->mergeCasts([ - 'is_admin' => 'integer', - 'options' => 'object', -]); -``` - -::: note -Attributes that are `null` will not be cast. In addition, you should never define a cast (or an attribute) that has the same name as a relationship. -::: - -### Array & JSON Casting - -The `array` cast is particularly useful when working with columns that are stored as serialized JSON. For example, if your database has a `JSON` or `TEXT` field type that contains serialized JSON, adding the `array` cast to that attribute will automatically deserialize the attribute to a PHP array when you access it on your Eloquent model: - -```php - 'array', - ]; -} -``` - -Once the cast is defined, you may access the `options` attribute and it will automatically be deserialized from JSON into a PHP array. When you set the value of the `options` attribute, the given array will automatically be serialized back into JSON for storage: - -```php -use App\Models\User; - -$user = User::find(1); - -$options = $user->options; - -$options['key'] = 'value'; - -$user->options = $options; - -$user->save(); -``` - -### Date Casting - -By default, Eloquent will cast the `created_at` and `updated_at` columns to instances of [Carbon](https://github.com/briannesbitt/Carbon), which extends the PHP `DateTime` class and provides an assortment of helpful methods. You may cast additional date attributes by defining additional date casts within your model's `$casts` property array. Typically, dates should be cast using the `datetime` or `immutable_datetime` cast types. - -When defining a `date` or `datetime` cast, you may also specify the date's format. This format will be used when the [model is serialized to an array or JSON](/docs/eloquent-serialization): - -```php -/** - * The attributes that should be cast. - */ -protected array $casts = [ - 'created_at' => 'datetime:Y-m-d', -]; -``` - -When a column is cast as a date, you may set the corresponding model attribute value to a UNIX timestamp, date string (`Y-m-d`), date-time string, or a `DateTime` / `Carbon` instance. The date's value will be correctly converted and stored in your database. - -You may customize the default serialization format for all of your model's dates by defining a `serializeDate` method on your model. This method does not affect how your dates are formatted for storage in the database: - -```php -/** - * Prepare a date for array / JSON serialization. - * - * @param \DateTimeInterface $date - * @return string - */ -protected function serializeDate(DateTimeInterface $date) -{ - return $date->format('Y-m-d'); -} -``` - -To specify the format that should be used when actually storing a model's dates within your database, you should define a `$dateFormat` property on your model: - -```php -/** - * The storage format of the model's date columns. - */ -protected string $dateFormat = 'U'; -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/eloquent-relationships.md b/_archive/docs/0.3/docs/eloquent-relationships.md deleted file mode 100644 index 8ac3f5a9b5..0000000000 --- a/_archive/docs/0.3/docs/eloquent-relationships.md +++ /dev/null @@ -1,1673 +0,0 @@ -# Eloquent: Relationships -[[toc]] - -## Introduction - -Database tables are often related to one another. For example, a blog post may have many comments, or an order could be related to the user who placed it. Eloquent makes managing and working with these relationships easy, and supports several different types of relationships: - -
    - -- [One To One](#one-to-one) -- [One To Many](#one-to-many) -- [Many To Many](#many-to-many) -- [Has One Through](#has-one-through) -- [Has Many Through](#has-many-through) -- [One To One (Polymorphic)](#one-to-one-polymorphic-relations) -- [One To Many (Polymorphic)](#one-to-many-polymorphic-relations) -- [Many To Many (Polymorphic)](#many-to-many-polymorphic-relations) - -
    - -::: note -Hypervel's Eloquent is a forked version in the Hyperf framework, which was derived from an early -::: - -## Defining Relationships - -Eloquent relationships are defined as methods on your Eloquent model classes. Since, like Eloquent models themselves, relationships also serve as powerful [query builders](/docs/queries), defining relationships as methods provides powerful method chaining and querying capabilities. For example, we may chain additional constraints on this `posts` relationship: - -```php -$user->posts()->where('active', 1)->get(); -``` - -But, before diving too deep into using relationships, let's learn how to define each type. - -::: note -Relationship names cannot collide with attribute names as that could lead to your model not being able to know which one to resolve. -::: - -### One To One - -A one-to-one relationship is a very basic relation. For example, a `User` model might be associated with one `Phone`. To define this relationship, we place a `phone` method on the `User` model. The `phone` method should call the `hasOne` method and return its result: - -```php -hasOne('App\Phone'); - } -} -``` - -The first argument passed to the `hasOne` method is the name of the related model. Once the relationship is defined, we may retrieve the related record using Eloquent's dynamic properties. Dynamic properties allow you to access relationship methods as if they were properties defined on the model: - -```php -$phone = User::find(1)->phone; -``` - -Eloquent determines the foreign key of the relationship based on the model name. In this case, the `Phone` model is automatically assumed to have a `user_id` foreign key. If you wish to override this convention, you may pass a second argument to the `hasOne` method: - -```php -return $this->hasOne('App\Phone', 'foreign_key'); -``` - -Additionally, Eloquent assumes that the foreign key should have a value matching the `id` (or the custom `$primaryKey`) column of the parent. In other words, Eloquent will look for the value of the user's `id` column in the `user_id` column of the `Phone` record. If you would like the relationship to use a value other than `id`, you may pass a third argument to the `hasOne` method specifying your custom key: - -```php -return $this->hasOne('App\Phone', 'foreign_key', 'local_key'); -``` - -#### Defining The Inverse Of The Relationship - -So, we can access the `Phone` model from our `User`. Now, let's define a relationship on the `Phone` model that will let us access the `User` that owns the phone. We can define the inverse of a `hasOne` relationship using the `belongsTo` method: - -```php -belongsTo('App\User'); - } -} -``` - -In the example above, Eloquent will try to match the `user_id` from the `Phone` model to an `id` on the `User` model. Eloquent determines the default foreign key name by examining the name of the relationship method and suffixing the method name with `_id`. However, if the foreign key on the `Phone` model is not `user_id`, you may pass a custom key name as the second argument to the `belongsTo` method: - -```php -/** - * Get the user that owns the phone. - */ -public function user() -{ - return $this->belongsTo('App\User', 'foreign_key'); -} -``` - -If your parent model does not use `id` as its primary key, or you wish to join the child model to a different column, you may pass a third argument to the `belongsTo` method specifying your parent table's custom key: - -```php -/** - * Get the user that owns the phone. - */ -public function user() -{ - return $this->belongsTo('App\User', 'foreign_key', 'other_key'); -} -``` - -### One To Many - -A one-to-many relationship is used to define relationships where a single model owns any amount of other models. For example, a blog post may have an infinite number of comments. Like all other Eloquent relationships, one-to-many relationships are defined by placing a function on your Eloquent model: - -```php -hasMany('App\Comment'); - } -} -``` - -Remember, Eloquent will automatically determine the proper foreign key column on the `Comment` model. By convention, Eloquent will take the "snake case" name of the owning model and suffix it with `_id`. So, for this example, Eloquent will assume the foreign key on the `Comment` model is `post_id`. - -Once the relationship has been defined, we can access the collection of comments by accessing the `comments` property. Remember, since Eloquent provides "dynamic properties", we can access relationship methods as if they were defined as properties on the model: - -```php -$comments = App\Post::find(1)->comments; - -foreach ($comments as $comment) { - // -} -``` - -Since all relationships also serve as query builders, you can add further constraints to which comments are retrieved by calling the `comments` method and continuing to chain conditions onto the query: - -```php -$comment = App\Post::find(1)->comments()->where('title', 'foo')->first(); -``` - -Like the `hasOne` method, you may also override the foreign and local keys by passing additional arguments to the `hasMany` method: - -```php -return $this->hasMany('App\Comment', 'foreign_key'); - -return $this->hasMany('App\Comment', 'foreign_key', 'local_key'); -``` - -### One To Many (Inverse) - -Now that we can access all of a post's comments, let's define a relationship to allow a comment to access its parent post. To define the inverse of a `hasMany` relationship, define a relationship function on the child model which calls the `belongsTo` method: - -```php -belongsTo('App\Post'); - } -} -``` - -Once the relationship has been defined, we can retrieve the `Post` model for a `Comment` by accessing the `post` "dynamic property": - -```php -$comment = App\Comment::find(1); - -echo $comment->post->title; -``` - -In the example above, Eloquent will try to match the `post_id` from the `Comment` model to an `id` on the `Post` model. Eloquent determines the default foreign key name by examining the name of the relationship method and suffixing the method name with a `_` followed by the name of the primary key column. However, if the foreign key on the `Comment` model is not `post_id`, you may pass a custom key name as the second argument to the `belongsTo` method: - -```php -/** - * Get the post that owns the comment. - */ -public function post() -{ - return $this->belongsTo('App\Post', 'foreign_key'); -} -``` - -If your parent model does not use `id` as its primary key, or you wish to join the child model to a different column, you may pass a third argument to the `belongsTo` method specifying your parent table's custom key: - -```php -/** - * Get the post that owns the comment. - */ -public function post() -{ - return $this->belongsTo('App\Post', 'foreign_key', 'other_key'); -} -``` - -### Many To Many - -Many-to-many relations are slightly more complicated than `hasOne` and `hasMany` relationships. An example of such a relationship is a user with many roles, where the roles are also shared by other users. For example, many users may have the role of "Admin". - -#### Table Structure - -To define this relationship, three database tables are needed: `users`, `roles`, and `role_user`. The `role_user` table is derived from the alphabetical order of the related model names, and contains the `user_id` and `role_id` columns: - -```text:no-line-numbers -users - id - integer - name - string - -roles - id - integer - name - string - -role_user - user_id - integer - role_id - integer -``` - -#### Model Structure - -Many-to-many relationships are defined by writing a method that returns the result of the `belongsToMany` method. For example, let's define the `roles` method on our `User` model: - -```php -belongsToMany('App\Role'); - } -} -``` - -Once the relationship is defined, you may access the user's roles using the `roles` dynamic property: - -```php -$user = App\User::find(1); - -foreach ($user->roles as $role) { - // -} -``` - -Like all other relationship types, you may call the `roles` method to continue chaining query constraints onto the relationship: - -```php -$roles = App\User::find(1)->roles()->orderBy('name')->get(); -``` - -As mentioned previously, to determine the table name of the relationship's joining table, Eloquent will join the two related model names in alphabetical order. However, you are free to override this convention. You may do so by passing a second argument to the `belongsToMany` method: - -```php -return $this->belongsToMany('App\Role', 'role_user'); -``` - -In addition to customizing the name of the joining table, you may also customize the column names of the keys on the table by passing additional arguments to the `belongsToMany` method. The third argument is the foreign key name of the model on which you are defining the relationship, while the fourth argument is the foreign key name of the model that you are joining to: - -```php -return $this->belongsToMany('App\Role', 'role_user', 'user_id', 'role_id'); -``` - -#### Defining The Inverse Of The Relationship - -To define the inverse of a many-to-many relationship, you place another call to `belongsToMany` on your related model. To continue our user roles example, let's define the `users` method on the `Role` model: - -```php -belongsToMany('App\User'); - } -} -``` - -As you can see, the relationship is defined exactly the same as its `User` counterpart, with the exception of referencing the `App\User` model. Since we're reusing the `belongsToMany` method, all of the usual table and key customization options are available when defining the inverse of many-to-many relationships. - -#### Retrieving Intermediate Table Columns - -As you have already learned, working with many-to-many relations requires the presence of an intermediate table. Eloquent provides some very helpful ways of interacting with this table. For example, let's assume our `User` object has many `Role` objects that it is related to. After accessing this relationship, we may access the intermediate table using the `pivot` attribute on the models: - -```php -$user = App\User::find(1); - -foreach ($user->roles as $role) { - echo $role->pivot->created_at; -} -``` - -Notice that each `Role` model we retrieve is automatically assigned a `pivot` attribute. This attribute contains a model representing the intermediate table, and may be used like any other Eloquent model. - -By default, only the model keys will be present on the `pivot` object. If your pivot table contains extra attributes, you must specify them when defining the relationship: - -```php -return $this->belongsToMany('App\Role')->withPivot('column1', 'column2'); -``` - -If you want your pivot table to have automatically maintained `created_at` and `updated_at` timestamps, use the `withTimestamps` method on the relationship definition: - -```php -return $this->belongsToMany('App\Role')->withTimestamps(); -``` - -#### Customizing The `pivot` Attribute Name - -As noted earlier, attributes from the intermediate table may be accessed on models using the `pivot` attribute. However, you are free to customize the name of this attribute to better reflect its purpose within your application. - -For example, if your application contains users that may subscribe to podcasts, you probably have a many-to-many relationship between users and podcasts. If this is the case, you may wish to rename your intermediate table accessor to `subscription` instead of `pivot`. This can be done using the `as` method when defining the relationship: - -```php -return $this->belongsToMany('App\Podcast') - ->as('subscription') - ->withTimestamps(); -``` - -Once this is done, you may access the intermediate table data using the customized name: - -```php - $users = User::with('podcasts')->get(); - - foreach ($users->flatMap->podcasts as $podcast) { - echo $podcast->subscription->created_at; - } -``` - -#### Filtering Relationships Via Intermediate Table Columns - -You can also filter the results returned by `belongsToMany` using the `wherePivot`, `wherePivotIn`, and `wherePivotNotIn` methods when defining the relationship: - -```php -return $this->belongsToMany('App\Role')->wherePivot('approved', 1); - -return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]); - -return $this->belongsToMany('App\Role')->wherePivotNotIn('priority', [1, 2]); -``` - -### Defining Custom Intermediate Table Models - -If you would like to define a custom model to represent the intermediate table of your relationship, you may call the `using` method when defining the relationship. Custom many-to-many pivot models should extend the `Hyperf\Database\Model\Relations\Pivot` class while custom polymorphic many-to-many pivot models should extend the `Hyperf\Database\Model\Relations\MorphPivot` class. For example, we may define a `Role` which uses a custom `RoleUser` pivot model: - -```php -belongsToMany('App\User')->using('App\RoleUser'); - } -} -``` - -When defining the `RoleUser` model, we will extend the `Pivot` class: - -```php -belongsToMany('App\User') - ->using('App\RoleUser') - ->withPivot([ - 'created_by', - 'updated_by', - ]); - } -} -``` - -::: note -Pivot models may not use the `SoftDeletes` trait. If you need to soft delete pivot records consider converting your pivot model to an actual Eloquent model. -::: - -#### Custom Pivot Models And Incrementing IDs - -If you have defined a many-to-many relationship that uses a custom pivot model, and that pivot model has an auto-incrementing primary key, you should ensure your custom pivot model class defines an `incrementing` property that is set to `true`. - -```php -/** - * Indicates if the IDs are auto-incrementing. - */ -public bool $incrementing = true; -``` - -### Has One Through - -The "has-one-through" relationship links models through a single intermediate relation. -For example, if each supplier has one user, and each user is associated with one user history record, then the supplier model may access the user's history _through_ the user. Let's look at the database tables necessary to define this relationship: - -```text:no-line-numbers -users - id - integer - supplier_id - integer - -suppliers - id - integer - -history - id - integer - user_id - integer -``` - -Though the `history` table does not contain a `supplier_id` column, the `hasOneThrough` relation can provide access to the user's history to the supplier model. Now that we have examined the table structure for the relationship, let's define it on the `Supplier` model: - -```php -hasOneThrough('App\History', 'App\User'); - } -} -``` - -The first argument passed to the `hasOneThrough` method is the name of the final model we wish to access, while the second argument is the name of the intermediate model. - -Typical Eloquent foreign key conventions will be used when performing the relationship's queries. If you would like to customize the keys of the relationship, you may pass them as the third and fourth arguments to the `hasOneThrough` method. The third argument is the name of the foreign key on the intermediate model. The fourth argument is the name of the foreign key on the final model. The fifth argument is the local key, while the sixth argument is the local key of the intermediate model: - -```php -class Supplier extends Model -{ - /** - * Get the user's history. - */ - public function userHistory() - { - return $this->hasOneThrough( - 'App\History', - 'App\User', - 'supplier_id', // Foreign key on users table... - 'user_id', // Foreign key on history table... - 'id', // Local key on suppliers table... - 'id' // Local key on users table... - ); - } -} -``` - -### Has Many Through - -The "has-many-through" relationship provides a convenient shortcut for accessing distant relations via an intermediate relation. For example, a `Country` model might have many `Post` models through an intermediate `User` model. In this example, you could easily gather all blog posts for a given country. Let's look at the tables required to define this relationship: - -```text:no-line-numbers -countries - id - integer - name - string - -users - id - integer - country_id - integer - name - string - -posts - id - integer - user_id - integer - title - string -``` - -Though `posts` does not contain a `country_id` column, the `hasManyThrough` relation provides access to a country's posts via `$country->posts`. To perform this query, Eloquent inspects the `country_id` on the intermediate `users` table. After finding the matching user IDs, they are used to query the `posts` table. - -Now that we have examined the table structure for the relationship, let's define it on the `Country` model: - -```php -hasManyThrough('App\Post', 'App\User'); - } -} -``` - -The first argument passed to the `hasManyThrough` method is the name of the final model we wish to access, while the second argument is the name of the intermediate model. - -Typical Eloquent foreign key conventions will be used when performing the relationship's queries. If you would like to customize the keys of the relationship, you may pass them as the third and fourth arguments to the `hasManyThrough` method. The third argument is the name of the foreign key on the intermediate model. The fourth argument is the name of the foreign key on the final model. The fifth argument is the local key, while the sixth argument is the local key of the intermediate model: - -```php -class Country extends Model -{ - public function posts() - { - return $this->hasManyThrough( - 'App\Post', - 'App\User', - 'country_id', // Foreign key on users table... - 'user_id', // Foreign key on posts table... - 'id', // Local key on countries table... - 'id' // Local key on users table... - ); - } -} -``` - -## Polymorphic Relationships - -A polymorphic relationship allows the target model to belong to more than one type of model using a single association. - - -### One To One (Polymorphic) - -#### Table Structure - -A one-to-one polymorphic relation is similar to a simple one-to-one relation; however, the target model can belong to more than one type of model on a single association. For example, a blog `Post` and a `User` may share a polymorphic relation to an `Image` model. Using a one-to-one polymorphic relation allows you to have a single list of unique images that are used for both blog posts and user accounts. First, let's examine the table structure: - -```text:no-line-numbers -posts - id - integer - name - string - -users - id - integer - name - string - -images - id - integer - url - string - imageable_id - integer - imageable_type - string -``` - -Take note of the `imageable_id` and `imageable_type` columns on the `images` table. The `imageable_id` column will contain the ID value of the post or user, while the `imageable_type` column will contain the class name of the parent model. The `imageable_type` column is used by Eloquent to determine which "type" of parent model to return when accessing the `imageable` relation. - -#### Model Structure - -Next, let's examine the model definitions needed to build this relationship: - -```php -morphTo(); - } -} - -class Post extends Model -{ - /** - * Get the post's image. - */ - public function image() - { - return $this->morphOne('App\Image', 'imageable'); - } -} - -class User extends Model -{ - /** - * Get the user's image. - */ - public function image() - { - return $this->morphOne('App\Image', 'imageable'); - } -} -``` - -#### Retrieving The Relationship - -Once your database table and models are defined, you may access the relationships via your models. For example, to retrieve the image for a post, we can use the `image` dynamic property: - -```php -$post = App\Post::find(1); - -$image = $post->image; -``` - -You may also retrieve the parent from the polymorphic model by accessing the name of the method that performs the call to `morphTo`. In our case, that is the `imageable` method on the `Image` model. So, we will access that method as a dynamic property: - -```php -$image = App\Image::find(1); - -$imageable = $image->imageable; -``` - -The `imageable` relation on the `Image` model will return either a `Post` or `User` instance, depending on which type of model owns the image. - - -### One To Many (Polymorphic) - -#### Table Structure - -A one-to-many polymorphic relation is similar to a simple one-to-many relation; however, the target model can belong to more than one type of model on a single association. For example, imagine users of your application can "comment" on both posts and videos. Using polymorphic relationships, you may use a single `comments` table for both of these scenarios. First, let's examine the table structure required to build this relationship: - -```text:no-line-numbers -posts - id - integer - title - string - body - text - -videos - id - integer - title - string - url - string - -comments - id - integer - body - text - commentable_id - integer - commentable_type - string -``` - -#### Model Structure - -Next, let's examine the model definitions needed to build this relationship: - -```php -morphTo(); - } -} - -class Post extends Model -{ - /** - * Get all of the post's comments. - */ - public function comments() - { - return $this->morphMany('App\Comment', 'commentable'); - } -} - -class Video extends Model -{ - /** - * Get all of the video's comments. - */ - public function comments() - { - return $this->morphMany('App\Comment', 'commentable'); - } -} -``` - -#### Retrieving The Relationship - -Once your database table and models are defined, you may access the relationships via your models. For example, to access all of the comments for a post, we can use the `comments` dynamic property: - -```php -$post = App\Post::find(1); - -foreach ($post->comments as $comment) { - // -} -``` - -You may also retrieve the owner of a polymorphic relation from the polymorphic model by accessing the name of the method that performs the call to `morphTo`. In our case, that is the `commentable` method on the `Comment` model. So, we will access that method as a dynamic property: - -```php -$comment = App\Comment::find(1); - -$commentable = $comment->commentable; -``` - -The `commentable` relation on the `Comment` model will return either a `Post` or `Video` instance, depending on which type of model owns the comment. - - -### Many To Many (Polymorphic) - -#### Table Structure - -Many-to-many polymorphic relations are slightly more complicated than `morphOne` and `morphMany` relationships. For example, a blog `Post` and `Video` model could share a polymorphic relation to a `Tag` model. Using a many-to-many polymorphic relation allows you to have a single list of unique tags that are shared across blog posts and videos. First, let's examine the table structure: - -```text:no-line-numbers -posts - id - integer - name - string - -videos - id - integer - name - string - -tags - id - integer - name - string - -taggables - tag_id - integer - taggable_id - integer - taggable_type - string -``` - -#### Model Structure - -Next, we're ready to define the relationships on the model. The `Post` and `Video` models will both have a `tags` method that calls the `morphToMany` method on the base Eloquent class: - -```php -morphToMany('App\Tag', 'taggable'); - } -} -``` - -#### Defining The Inverse Of The Relationship - -Next, on the `Tag` model, you should define a method for each of its related models. So, for this example, we will define a `posts` method and a `videos` method: - -```php -morphedByMany('App\Post', 'taggable'); - } - - /** - * Get all of the videos that are assigned this tag. - */ - public function videos() - { - return $this->morphedByMany('App\Video', 'taggable'); - } -} -``` - -#### Retrieving The Relationship - -Once your database table and models are defined, you may access the relationships via your models. For example, to access all of the tags for a post, you can use the `tags` dynamic property: - -```php -$post = App\Post::find(1); - -foreach ($post->tags as $tag) { - // -} -``` - -You may also retrieve the owner of a polymorphic relation from the polymorphic model by accessing the name of the method that performs the call to `morphedByMany`. In our case, that is the `posts` or `videos` methods on the `Tag` model. So, you will access those methods as dynamic properties: - -```php -$tag = App\Tag::find(1); - -foreach ($tag->videos as $video) { - // -} -``` - -### Custom Polymorphic Types - -By default, Laravel will use the fully qualified class name to store the type of the related model. For instance, given the one-to-many example above where a `Comment` may belong to a `Post` or a `Video`, the default `commentable_type` would be either `App\Post` or `App\Video`, respectively. However, you may wish to decouple your database from your application's internal structure. In that case, you may define a "morph map" to instruct Eloquent to use a custom name for each model instead of the class name: - -```php -use Hyperf\Database\Model\Relations\Relation; - -Relation::morphMap([ - 'posts' => 'App\Post', - 'videos' => 'App\Video', -]); -``` - -You may register the `morphMap` in the `boot` function of your `AppServiceProvider` or create a separate service provider if you wish. - -::: note -When adding a "morph map" to your existing application, every morphable :::`*_type` column value in your database that still contains a fully-qualified -class will need to be converted to its "map" name. -::: - -## Querying Relations - -Since all types of Eloquent relationships are defined via methods, you may call those methods to obtain an instance of the relationship without actually executing the relationship queries. In addition, all types of Eloquent relationships also serve as [query builders](/docs/queries), allowing you to continue to chain constraints onto the relationship query before finally executing the SQL against your database. - -For example, imagine a blog system in which a `User` model has many associated `Post` models: - -```php -hasMany('App\Post'); - } -} -``` - -You may query the `posts` relationship and add additional constraints to the relationship like so: - -```php -$user = App\User::find(1); - -$user->posts()->where('active', 1)->get(); -``` - -You are able to use any of the [query builder](/docs/queries) methods on the relationship, so be sure to explore the query builder documentation to learn about all of the methods that are available to you. - -#### Chaining `orWhere` Clauses After Relationships - -As demonstrated in the example above, you are free to add additional constraints to relationships when querying them. However, use caution when chaining `orWhere` clauses onto a relationship, as the `orWhere` clauses will be logically grouped at the same level as the relationship constraint: - -```php -$user->posts() - ->where('active', 1) - ->orWhere('votes', '>=', 100) - ->get(); - -// select * from posts -// where user_id = ? and active = 1 or votes >= 100 -``` - -In most situations, you likely intend to use [constraint groups](/docs/queries#parameter-grouping) to logically group the conditional checks between parentheses: - -```php -use Hyperf\Database\Model\Builder; - -$user->posts() - ->where(function (Builder $query) { - return $query->where('active', 1) - ->orWhere('votes', '>=', 100); - }) - ->get(); - -// select * from posts -// where user_id = ? and (active = 1 or votes >= 100) -``` - -### Relationship Methods Vs. Dynamic Properties - -If you do not need to add additional constraints to an Eloquent relationship query, you may access the relationship as if it were a property. For example, continuing to use our `User` and `Post` example models, we may access all of a user's posts like so: - -```php -$user = App\User::find(1); - -foreach ($user->posts as $post) { - // -} -``` - -Dynamic properties are "lazy loading", meaning they will only load their relationship data when you actually access them. Because of this, developers often use [eager loading](#eager-loading) to pre-load relationships they know will be accessed after loading the model. Eager loading provides a significant reduction in SQL queries that must be executed to load a model's relations. - -### Querying Relationship Existence - -When accessing the records for a model, you may wish to limit your results based on the existence of a relationship. For example, imagine you want to retrieve all blog posts that have at least one comment. To do so, you may pass the name of the relationship to the `has` and `orHas` methods: - -```php -// Retrieve all posts that have at least one comment... -$posts = App\Post::has('comments')->get(); -``` - -You may also specify an operator and count to further customize the query: - -```php -// Retrieve all posts that have three or more comments... -$posts = App\Post::has('comments', '>=', 3)->get(); -``` - -Nested `has` statements may also be constructed using "dot" notation. For example, you may retrieve all posts that have at least one comment and vote: - -```php -// Retrieve posts that have at least one comment with votes... -$posts = App\Post::has('comments.votes')->get(); -``` - -If you need even more power, you may use the `whereHas` and `orWhereHas` methods to put "where" conditions on your `has` queries. These methods allow you to add customized constraints to a relationship constraint, such as checking the content of a comment: - -```php -use Hyperf\Database\Model\Builder; - -// Retrieve posts with at least one comment containing words like foo%... -$posts = App\Post::whereHas('comments', function (Builder $query) { - $query->where('content', 'like', 'foo%'); -})->get(); - -// Retrieve posts with at least ten comments containing words like foo%... -$posts = App\Post::whereHas('comments', function (Builder $query) { - $query->where('content', 'like', 'foo%'); -}, '>=', 10)->get(); -``` - -### Querying Relationship Absence - -When accessing the records for a model, you may wish to limit your results based on the absence of a relationship. For example, imagine you want to retrieve all blog posts that **don't** have any comments. To do so, you may pass the name of the relationship to the `doesntHave` and `orDoesntHave` methods: - -```php -$posts = App\Post::doesntHave('comments')->get(); -``` - -If you need even more power, you may use the `whereDoesntHave` and `orWhereDoesntHave` methods to put "where" conditions on your `doesntHave` queries. These methods allows you to add customized constraints to a relationship constraint, such as checking the content of a comment: - -```php -use Hyperf\Database\Model\Builder; - -$posts = App\Post::whereDoesntHave('comments', function (Builder $query) { - $query->where('content', 'like', 'foo%'); -})->get(); -``` - -You may use "dot" notation to execute a query against a nested relationship. For example, the following query will retrieve all posts with comments from authors that are not banned: - -```php -use Hyperf\Database\Model\Builder; - -$posts = App\Post::whereDoesntHave('comments.author', function (Builder $query) { - $query->where('banned', 0); -})->get(); -``` - -### Querying Polymorphic Relationships - -To query the existence of `MorphTo` relationships, you may use the `whereHasMorph` method and its corresponding methods: - -```php -use Hyperf\Database\Model\Builder; - -// Retrieve comments associated to posts or videos with a title like foo%... -$comments = App\Comment::whereHasMorph( - 'commentable', - ['App\Post', 'App\Video'], - function (Builder $query) { - $query->where('title', 'like', 'foo%'); - } -)->get(); - -// Retrieve comments associated to posts with a title not like foo%... -$comments = App\Comment::whereDoesntHaveMorph( - 'commentable', - 'App\Post', - function (Builder $query) { - $query->where('title', 'like', 'foo%'); - } -)->get(); -``` - -You may use the `$type` parameter to add different constraints depending on the related model: - -```php -use Hyperf\Database\Model\Builder; - -$comments = App\Comment::whereHasMorph( - 'commentable', - ['App\Post', 'App\Video'], - function (Builder $query, $type) { - $query->where('title', 'like', 'foo%'); - - if ($type === 'App\Post') { - $query->orWhere('content', 'like', 'foo%'); - } - } -)->get(); -``` - -Instead of passing an array of possible polymorphic models, you may provide `*` as a wildcard and let Laravel retrieve all the possible polymorphic types from the database. Laravel will execute an additional query in order to perform this operation: - -```php -use Hyperf\Database\Model\Builder; - -$comments = App\Comment::whereHasMorph('commentable', '*', function (Builder $query) { - $query->where('title', 'like', 'foo%'); -})->get(); -``` - -### Counting Related Models - -If you want to count the number of results from a relationship without actually loading them you may use the `withCount` method, which will place a `{relation}_count` column on your resulting models. For example: - -```php -$posts = App\Post::withCount('comments')->get(); - -foreach ($posts as $post) { - echo $post->comments_count; -} -``` - -You may add the "counts" for multiple relations as well as add constraints to the queries: - -```php -use Hyperf\Database\Model\Builder; - -$posts = App\Post::withCount(['votes', 'comments' => function (Builder $query) { - $query->where('content', 'like', 'foo%'); -}])->get(); - -echo $posts[0]->votes_count; -echo $posts[0]->comments_count; -``` - -You may also alias the relationship count result, allowing multiple counts on the same relationship: - -```php -use Hyperf\Database\Model\Builder; - -$posts = App\Post::withCount([ - 'comments', - 'comments as pending_comments_count' => function (Builder $query) { - $query->where('approved', false); - }, -])->get(); - -echo $posts[0]->comments_count; - -echo $posts[0]->pending_comments_count; -``` - -If you're combining `withCount` with a `select` statement, ensure that you call `withCount` after the `select` method: - -```php -$posts = App\Post::select(['title', 'body'])->withCount('comments')->get(); - -echo $posts[0]->title; -echo $posts[0]->body; -echo $posts[0]->comments_count; -``` - -In addition, using the `loadCount` method, you may load a relationship count after the parent model has already been retrieved: - -```php -$book = App\Book::first(); - -$book->loadCount('genres'); -``` - -If you need to set additional query constraints on the eager loading query, you may pass an array keyed by the relationships you wish to load. The array values should be `Closure` instances which receive the query builder instance: - -```php -$book->loadCount(['reviews' => function ($query) { - $query->where('rating', 5); -}]) -``` - -## Eager Loading - -When accessing Eloquent relationships as properties, the relationship data is "lazy loaded". This means the relationship data is not actually loaded until you first access the property. However, Eloquent can "eager load" relationships at the time you query the parent model. Eager loading alleviates the N + 1 query problem. To illustrate the N + 1 query problem, consider a `Book` model that is related to `Author`: - -```php -belongsTo('App\Author'); - } -} -``` - -Now, let's retrieve all books and their authors: - -```php -$books = App\Book::all(); - -foreach ($books as $book) { - echo $book->author->name; -} -``` - -This loop will execute 1 query to retrieve all of the books on the table, then another query for each book to retrieve the author. So, if we have 25 books, this loop would run 26 queries: 1 for the original book, and 25 additional queries to retrieve the author of each book. - -Thankfully, we can use eager loading to reduce this operation to just 2 queries. When querying, you may specify which relationships should be eager loaded using the `with` method: - -```php -$books = App\Book::with('author')->get(); - -foreach ($books as $book) { - echo $book->author->name; -} -``` - -For this operation, only two queries will be executed: - -```text:no-line-numbers -select * from books - -select * from authors where id in (1, 2, 3, 4, 5, ...) -``` - -#### Eager Loading Multiple Relationships - -Sometimes you may need to eager load several different relationships in a single operation. To do so, just pass additional arguments to the `with` method: - -```php -$books = App\Book::with(['author', 'publisher'])->get(); -``` - -#### Nested Eager Loading - -To eager load nested relationships, you may use "dot" syntax. For example, let's eager load all of the book's authors and all of the author's personal contacts in one Eloquent statement: - -```php -$books = App\Book::with('author.contacts')->get(); -``` - -#### Nested Eager Loading `morphTo` Relationships - -If you would like to eager load a `morphTo` relationship, as well as nested relationships on the various entities that may be returned by that relationship, you may use the `with` method in combination with the `morphTo` relationship's `morphWith` method. To help illustrate this method, let's consider the following model: - -```php -morphTo(); - } -} -``` - -In this example, let's assume `Event`, `Photo`, and `Post` models may create `ActivityFeed` models. Additionally, let's assume that `Event` models belong to a `Calendar` model, `Photo` models are associated with `Tag` models, and `Post` models belong to an `Author` model. - -Using these model definitions and relationships, we may retrieve `ActivityFeed` model instances and eager load all `parentable` models and their respective nested relationships: - -```php -use Hyperf\Database\Model\Relations\MorphTo; - -$activities = ActivityFeed::query() - ->with(['parentable' => function (MorphTo $morphTo) { - $morphTo->morphWith([ - Event::class => ['calendar'], - Photo::class => ['tags'], - Post::class => ['author'], - ]); - }])->get(); -``` - -#### Eager Loading Specific Columns - -You may not always need every column from the relationships you are retrieving. For this reason, Eloquent allows you to specify which columns of the relationship you would like to retrieve: - -```php -$books = App\Book::with('author:id,name')->get(); -``` - -::: note -When using this feature, you should always include the `id` column and any relevant foreign key columns in the list of columns you wish to retrieve. -::: - -#### Eager Loading By Default - -Sometimes you might want to always load some relationships when retrieving a model. To accomplish this, you may define a `$with` property on the model: - -```php -belongsTo('App\Author'); - } -} -``` - -If you would like to remove an item from the `$with` property for a single query, you may use the `without` method: - -```php -$books = App\Book::without('author')->get(); -``` - -### Constraining Eager Loads - -Sometimes you may wish to eager load a relationship, but also specify additional query conditions for the eager loading query. Here's an example: - -```php -$users = App\User::with(['posts' => function ($query) { - $query->where('title', 'like', '%first%'); -}])->get(); -``` - -In this example, Eloquent will only eager load posts where the post's `title` column contains the word `first`. You may call other [query builder](/docs/queries) methods to further customize the eager loading operation: - -```php -$users = App\User::with(['posts' => function ($query) { - $query->orderBy('created_at', 'desc'); -}])->get(); -``` - -::: note -The `limit` and `take` query builder methods may not be used when constraining eager loads. -::: - -### Lazy Eager Loading - -Sometimes you may need to eager load a relationship after the parent model has already been retrieved. For example, this may be useful if you need to dynamically decide whether to load related models: - -```php -$books = App\Book::all(); - -if ($someCondition) { - $books->load('author', 'publisher'); -} -``` - -If you need to set additional query constraints on the eager loading query, you may pass an array keyed by the relationships you wish to load. The array values should be `Closure` instances which receive the query instance: - -```php -$author->load(['books' => function ($query) { - $query->orderBy('published_date', 'asc'); -}]); -``` - -To load a relationship only when it has not already been loaded, use the `loadMissing` method: - -```php -public function format(Book $book) -{ - $book->loadMissing('author'); - - return [ - 'name' => $book->name, - 'author' => $book->author->name, - ]; -} -``` - -#### Nested Lazy Eager Loading & `morphTo` - -If you would like to eager load a `morphTo` relationship, as well as nested relationships on the various entities that may be returned by that relationship, you may use the `loadMorph` method. - -This method accepts the name of the `morphTo` relationship as its first argument, and an array of model / relationship pairs as its second argument. To help illustrate this method, let's consider the following model: - -```php -morphTo(); - } -} -``` - -In this example, let's assume `Event`, `Photo`, and `Post` models may create `ActivityFeed` models. Additionally, let's assume that `Event` models belong to a `Calendar` model, `Photo` models are associated with `Tag` models, and `Post` models belong to an `Author` model. - -Using these model definitions and relationships, we may retrieve `ActivityFeed` model instances and eager load all `parentable` models and their respective nested relationships: - -```php -$activities = ActivityFeed::with('parentable') - ->get() - ->loadMorph('parentable', [ - Event::class => ['calendar'], - Photo::class => ['tags'], - Post::class => ['author'], - ]); -``` - -## Inserting & Updating Related Models - -### The Save Method - -Eloquent provides convenient methods for adding new models to relationships. For example, perhaps you need to insert a new `Comment` for a `Post` model. Instead of manually setting the `post_id` attribute on the `Comment`, you may insert the `Comment` directly from the relationship's `save` method: - -```php -$comment = new App\Comment(['message' => 'A new comment.']); - -$post = App\Post::find(1); - -$post->comments()->save($comment); -``` - -Notice that we did not access the `comments` relationship as a dynamic property. Instead, we called the `comments` method to obtain an instance of the relationship. The `save` method will automatically add the appropriate `post_id` value to the new `Comment` model. - -If you need to save multiple related models, you may use the `saveMany` method: - -```php -$post = App\Post::find(1); - -$post->comments()->saveMany([ - new App\Comment(['message' => 'A new comment.']), - new App\Comment(['message' => 'Another comment.']), -]); -``` - -#### Recursively Saving Models & Relationships - -If you would like to `save` your model and all of its associated relationships, you may use the `push` method: - -```php -$post = App\Post::find(1); - -$post->comments[0]->message = 'Message'; -$post->comments[0]->author->name = 'Author Name'; - -$post->push(); -``` - -### The Create Method - -In addition to the `save` and `saveMany` methods, you may also use the `create` method, which accepts an array of attributes, creates a model, and inserts it into the database. Again, the difference between `save` and `create` is that `save` accepts a full Eloquent model instance while `create` accepts a plain PHP `array`: - -```php -$post = App\Post::find(1); - -$comment = $post->comments()->create([ - 'message' => 'A new comment.', -]); -``` - -::: tip -Before using the `create` method, be sure to review the documentation on attribute [mass assignment](/docs/eloquent#mass-assignment). -::: - -You may use the `createMany` method to create multiple related models: - -```php -$post = App\Post::find(1); - -$post->comments()->createMany([ - [ - 'message' => 'A new comment.', - ], - [ - 'message' => 'Another new comment.', - ], -]); -``` - -You may also use the `findOrNew`, `firstOrNew`, `firstOrCreate` and `updateOrCreate` methods to [create and update models on relationships](https://laravel.com/docs/eloquent#other-creation-methods). - -### Belongs To Relationships - -When updating a `belongsTo` relationship, you may use the `associate` method. This method will set the foreign key on the child model: - -```php -$account = App\Account::find(10); - -$user->account()->associate($account); - -$user->save(); -``` - -When removing a `belongsTo` relationship, you may use the `dissociate` method. This method will set the relationship's foreign key to `null`: - -```php -$user->account()->dissociate(); - -$user->save(); -``` - -#### Default Models - -The `belongsTo`, `hasOne`, `hasOneThrough`, and `morphOne` relationships allow you to define a default model that will be returned if the given relationship is `null`. This pattern is often referred to as the [Null Object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern) and can help remove conditional checks in your code. In the following example, the `user` relation will return an empty `App\User` model if no `user` is attached to the post: - -```php -/** - * Get the author of the post. - */ -public function user() -{ - return $this->belongsTo('App\User')->withDefault(); -} -``` - -To populate the default model with attributes, you may pass an array or Closure to the `withDefault` method: - -```php -/** - * Get the author of the post. - */ -public function user() -{ - return $this->belongsTo('App\User')->withDefault([ - 'name' => 'Guest Author', - ]); -} - -/** - * Get the author of the post. - */ -public function user() -{ - return $this->belongsTo('App\User')->withDefault(function ($user, $post) { - $user->name = 'Guest Author'; - }); -} -``` - -### Many To Many Relationships - -#### Attaching / Detaching - -Eloquent also provides a few additional helper methods to make working with related models more convenient. For example, let's imagine a user can have many roles and a role can have many users. To attach a role to a user by inserting a record in the intermediate table that joins the models, use the `attach` method: - -```php -$user = App\User::find(1); - -$user->roles()->attach($roleId); -``` - -When attaching a relationship to a model, you may also pass an array of additional data to be inserted into the intermediate table: - -```php -$user->roles()->attach($roleId, ['expires' => $expires]); -``` - -Sometimes it may be necessary to remove a role from a user. To remove a many-to-many relationship record, use the `detach` method. The `detach` method will delete the appropriate record out of the intermediate table; however, both models will remain in the database: - -```php -// Detach a single role from the user... -$user->roles()->detach($roleId); - -// Detach all roles from the user... -$user->roles()->detach(); -``` - -For convenience, `attach` and `detach` also accept arrays of IDs as input: - -```php -$user = App\User::find(1); - -$user->roles()->detach([1, 2, 3]); - -$user->roles()->attach([ - 1 => ['expires' => $expires], - 2 => ['expires' => $expires], -]); -``` - -#### Syncing Associations - -You may also use the `sync` method to construct many-to-many associations. The `sync` method accepts an array of IDs to place on the intermediate table. Any IDs that are not in the given array will be removed from the intermediate table. So, after this operation is complete, only the IDs in the given array will exist in the intermediate table: - -```php -$user->roles()->sync([1, 2, 3]); -``` - -You may also pass additional intermediate table values with the IDs: - -```php -$user->roles()->sync([1 => ['expires' => true], 2, 3]); -``` - -If you do not want to detach existing IDs, you may use the `syncWithoutDetaching` method: - -```php -$user->roles()->syncWithoutDetaching([1, 2, 3]); -``` - -#### Toggling Associations - -The many-to-many relationship also provides a `toggle` method which "toggles" the attachment status of the given IDs. If the given ID is currently attached, it will be detached. Likewise, if it is currently detached, it will be attached: - -```php -$user->roles()->toggle([1, 2, 3]); -``` - -#### Saving Additional Data On A Pivot Table - -When working with a many-to-many relationship, the `save` method accepts an array of additional intermediate table attributes as its second argument: - -```php -App\User::find(1)->roles()->save($role, ['expires' => $expires]); -``` - -#### Updating A Record On A Pivot Table - -If you need to update an existing row in your pivot table, you may use `updateExistingPivot` method. This method accepts the pivot record foreign key and an array of attributes to update: - -```php -$user = App\User::find(1); - -$user->roles()->updateExistingPivot($roleId, $attributes); -``` - -## Touching Parent Timestamps - -When a model `belongsTo` or `belongsToMany` another model, such as a `Comment` which belongs to a `Post`, it is sometimes helpful to update the parent's timestamp when the child model is updated. For example, when a `Comment` model is updated, you may want to automatically "touch" the `updated_at` timestamp of the owning `Post`. Eloquent makes it easy. Just add a `touches` property containing the names of the relationships to the child model: - -```php -belongsTo('App\Post'); - } -} -``` - -Now, when you update a `Comment`, the owning `Post` will have its `updated_at` column updated as well, making it more convenient to know when to invalidate a cache of the `Post` model: - -```php -$comment = App\Comment::find(1); - -$comment->text = 'Edit to this comment!'; - -$comment->save(); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/eloquent-resources.md b/_archive/docs/0.3/docs/eloquent-resources.md deleted file mode 100644 index 56502df126..0000000000 --- a/_archive/docs/0.3/docs/eloquent-resources.md +++ /dev/null @@ -1,703 +0,0 @@ -# Eloquent: API Resources -[[toc]] - -## Introduction - -When building an API, you may need a transformation layer that sits between your Eloquent models and the JSON responses that are actually returned to your application's users. Hypervel's resource classes allow you to expressively and easily transform your models and model collections into JSON. - -## Generating Resources - -To generate a resource class, you may use the `make:resource` Artisan command. By default, resources will be placed in the `app/Http/Resources` directory of your application. Resources extend the `Hypervel\Http\Resources\Json\JsonResource` class: - -```shell:no-line-numbers -php artisan make:resource User -``` - -#### Resource Collections - -In addition to generating resources that transform individual models, you may generate resources that are responsible for transforming collections of models. This allows your response to include links and other meta information that is relevant to an entire collection of a given resource. - -To create a resource collection, you should use the `--collection` flag when creating the resource. Or, including the word `Collection` in the resource name will indicate to Hypervel that it should create a collection resource. Collection resources extend the `Hypervel\Http\Resources\Json\ResourceCollection` class: - -```shell:no-line-numbers -php artisan make:resource Users --collection - -php artisan make:resource UserCollection -``` - -## Concept Overview - -::: tip -This is a high-level overview of resources and resource collections. You are highly encouraged to read the other sections of this documentation to gain a deeper understanding of the customization and power offered to you by resources. -::: - -Before diving into all of the options available to you when writing resources, let's first take a high-level look at how resources are used within Hypervel. A resource class represents a single model that needs to be transformed into a JSON structure. For example, here is a simple `User` resource class: - -```php - $this->id, - 'name' => $this->name, - 'email' => $this->email, - 'created_at' => $this->created_at, - 'updated_at' => $this->updated_at, - ]; - } -} -``` - -Every resource class defines a `toArray` method which returns the array of attributes that should be converted to JSON when sending the response. Notice that we can access model properties directly from the `$this` variable. This is because a resource class will automatically proxy property and method access down to the underlying model for convenient access. Once the resource is defined, it may be returned from a route or controller: - -```php -use App\Http\Resources\User as UserResource; -use App\User; - -Route::get('/user', function () { - return new UserResource(User::find(1)); -}); -``` - -### Resource Collections - -If you are returning a collection of resources or a paginated response, you may use the `collection` method when creating the resource instance in your route or controller: - -```php -use App\Http\Resources\User as UserResource; -use App\User; - -Route::get('/user', function () { - return UserResource::collection(User::all()); -}); -``` - -Note that this does not allow any addition of meta data that may need to be returned with the collection. If you would like to customize the resource collection response, you may create a dedicated resource to represent the collection: - -```shell:no-line-numbers -php artisan make:resource UserCollection -``` - -Once the resource collection class has been generated, you may easily define any meta data that should be included with the response: - -```php - $this->collection, - 'links' => [ - 'self' => 'link-value', - ], - ]; - } -} -``` - -After defining your resource collection, it may be returned from a route or controller: - -```php -use App\Http\Resources\UserCollection; -use App\User; - -Route::get('/users', function () { - return new UserCollection(User::all()); -}); -``` - -#### Preserving Collection Keys - -When returning a resource collection from a route, Hypervel resets the collection's keys so that they are in simple numerical order. However, you may add a `preserveKeys` property to your resource class indicating if collection keys should be preserved: - -```php -keyBy->id); -}); -``` - -#### Customizing The Underlying Resource Class - -Typically, the `$this->collection` property of a resource collection is automatically populated with the result of mapping each item of the collection to its singular resource class. The singular resource class is assumed to be the collection's class name without the trailing `Collection` string. - -For example, `UserCollection` will attempt to map the given user instances into the `User` resource. To customize this behavior, you may override the `$collects` property of your resource collection: - -```php - $this->id, - 'name' => $this->name, - 'email' => $this->email, - 'created_at' => $this->created_at, - 'updated_at' => $this->updated_at, - ]; - } -} -``` - -Once a resource has been defined, it may be returned directly from a route or controller: - -```php -use App\Http\Resources\User as UserResource; -use App\User; - -Route::get('/user', function () { - return new UserResource(User::find(1)); -}); -``` - -#### Relationships - -If you would like to include related resources in your response, you may add them to the array returned by your `toArray` method. In this example, we will use the `Post` resource's `collection` method to add the user's blog posts to the resource response: - -```php -/** - * Transform the resource into an array. - * - * @return array - */ -public function toArray() -{ - return [ - 'id' => $this->id, - 'name' => $this->name, - 'email' => $this->email, - 'posts' => PostResource::collection($this->posts), - 'created_at' => $this->created_at, - 'updated_at' => $this->updated_at, - ]; -} -``` - -::: tip -If you would like to include relationships only when they have already been loaded, check out the documentation on [conditional relationships](#conditional-relationships). -::: - -#### Resource Collections - -While resources translate a single model into an array, resource collections translate a collection of models into an array. It is not absolutely necessary to define a resource collection class for each one of your model types since all resources provide a `collection` method to generate an "ad-hoc" resource collection on the fly: - -```php -use App\Http\Resources\User as UserResource; -use App\User; - -Route::get('/user', function () { - return UserResource::collection(User::all()); -}); -``` - -However, if you need to customize the meta data returned with the collection, it will be necessary to define a resource collection: - -```php - $this->collection, - 'links' => [ - 'self' => 'link-value', - ], - ]; - } -} -``` - -Like singular resources, resource collections may be returned directly from routes or controllers: - -```php -use App\Http\Resources\UserCollection; -use App\User; - -Route::get('/users', function () { - return new UserCollection(User::all()); -}); -``` - -### Data Wrapping - -By default, your outermost resource is wrapped in a `data` key when the resource response is converted to JSON. So, for example, a typical resource collection response looks like the following: - -```json -{ - "data": [ - { - "id": 1, - "name": "Eladio Schroeder Sr.", - "email": "therese28@example.com", - }, - { - "id": 2, - "name": "Liliana Mayert", - "email": "evandervort@example.com", - } - ] -} -``` - -If you would like to disable the wrapping of the outermost resource, you may use the `withoutWrapping` method on the base resource class: - -```php -withoutWrapping() - ->toResponse(); - } -} -``` - -::: note -The `withoutWrapping` method only affects the outermost response and will not remove `data` keys that you manually add to your own resource collections. -::: - -### Wrapping Nested Resources - -You have total freedom to determine how your resource's relationships are wrapped. If you would like all resource collections to be wrapped in a `data` key, regardless of their nesting, you should define a resource collection class for each resource and return the collection within a `data` key. - -You may be wondering if this will cause your outermost resource to be wrapped in two `data` keys. Don't worry, Hypervel will never let your resources be accidentally double-wrapped, so you don't have to be concerned about the nesting level of the resource collection you are transforming: - -```php - $this->collection]; - } -} -``` - -### Data Wrapping And Pagination - -When returning paginated collections in a resource response, Hypervel will wrap your resource data in a `data` key even if the `withoutWrapping` method has been called. This is because paginated responses always contain `meta` and `links` keys with information about the paginator's state: - -```json -{ - "data": [ - { - "id": 1, - "name": "Eladio Schroeder Sr.", - "email": "therese28@example.com", - }, - { - "id": 2, - "name": "Liliana Mayert", - "email": "evandervort@example.com", - } - ], - "links":{ - "first": "http://example.com/pagination?page=1", - "last": "http://example.com/pagination?page=1", - "prev": null, - "next": null - }, - "meta":{ - "current_page": 1, - "from": 1, - "last_page": 1, - "path": "http://example.com/pagination", - "per_page": 15, - "to": 10, - "total": 10 - } -} -``` - -### Pagination - -You may always pass a paginator instance to the `collection` method of a resource or to a custom resource collection: - -```php -use App\Http\Resources\UserCollection; -use App\User; - -Route::get('/users', function () { - return new UserCollection(User::paginate()); -}); -``` - -Paginated responses always contain `meta` and `links` keys with information about the paginator's state: - -```json -{ - "data": [ - { - "id": 1, - "name": "Eladio Schroeder Sr.", - "email": "therese28@example.com", - }, - { - "id": 2, - "name": "Liliana Mayert", - "email": "evandervort@example.com", - } - ], - "links":{ - "first": "http://example.com/pagination?page=1", - "last": "http://example.com/pagination?page=1", - "prev": null, - "next": null - }, - "meta":{ - "current_page": 1, - "from": 1, - "last_page": 1, - "path": "http://example.com/pagination", - "per_page": 15, - "to": 10, - "total": 10 - } -} -``` - -### Conditional Attributes - -Sometimes you may wish to only include an attribute in a resource response if a given condition is met. For example, you may wish to only include a value if the current user is an "administrator". Hypervel provides a variety of helper methods to assist you in this situation. The `when` method may be used to conditionally add an attribute to a resource response: - -```php -/** - * Transform the resource into an array. - * - * @return array - */ -public function toArray() -{ - return [ - 'id' => $this->id, - 'name' => $this->name, - 'email' => $this->email, - 'secret' => $this->when(Auth::user()->isAdmin(), 'secret-value'), - 'created_at' => $this->created_at, - 'updated_at' => $this->updated_at, - ]; -} -``` - -In this example, the `secret` key will only be returned in the final resource response if the authenticated user's `isAdmin` method returns `true`. If the method returns `false`, the `secret` key will be removed from the resource response entirely before it is sent back to the client. The `when` method allows you to expressively define your resources without resorting to conditional statements when building the array. - -The `when` method also accepts a Closure as its second argument, allowing you to calculate the resulting value only if the given condition is `true`: - -```php -'secret' => $this->when(Auth::user()->isAdmin(), function () { - return 'secret-value'; -}), -``` - -#### Merging Conditional Attributes - -Sometimes you may have several attributes that should only be included in the resource response based on the same condition. In this case, you may use the `mergeWhen` method to include the attributes in the response only when the given condition is `true`: - -```php -/** - * Transform the resource into an array. - * - * @return array - */ -public function toArray() -{ - return [ - 'id' => $this->id, - 'name' => $this->name, - 'email' => $this->email, - $this->mergeWhen(Auth::user()->isAdmin(), [ - 'first-secret' => 'value', - 'second-secret' => 'value', - ]), - 'created_at' => $this->created_at, - 'updated_at' => $this->updated_at, - ]; -} -``` - -Again, if the given condition is `false`, these attributes will be removed from the resource response entirely before it is sent to the client. - -::: note -The `mergeWhen` method should not be used within arrays that mix string and numeric keys. Furthermore, it should not be used within arrays with numeric keys that are not ordered sequentially. -::: - -### Conditional Relationships - -In addition to conditionally loading attributes, you may conditionally include relationships on your resource responses based on if the relationship has already been loaded on the model. This allows your controller to decide which relationships should be loaded on the model and your resource can easily include them only when they have actually been loaded. - -Ultimately, this makes it easier to avoid "N+1" query problems within your resources. The `whenLoaded` method may be used to conditionally load a relationship. In order to avoid unnecessarily loading relationships, this method accepts the name of the relationship instead of the relationship itself: - -```php -/** - * Transform the resource into an array. - * - * @return array - */ -public function toArray() -{ - return [ - 'id' => $this->id, - 'name' => $this->name, - 'email' => $this->email, - 'posts' => PostResource::collection($this->whenLoaded('posts')), - 'created_at' => $this->created_at, - 'updated_at' => $this->updated_at, - ]; -} -``` - -In this example, if the relationship has not been loaded, the `posts` key will be removed from the resource response entirely before it is sent to the client. - -#### Conditional Pivot Information - -In addition to conditionally including relationship information in your resource responses, you may conditionally include data from the intermediate tables of many-to-many relationships using the `whenPivotLoaded` method. The `whenPivotLoaded` method accepts the name of the pivot table as its first argument. The second argument should be a Closure that defines the value to be returned if the pivot information is available on the model: - -```php -/** - * Transform the resource into an array. - * - * @return array - */ -public function toArray() -{ - return [ - 'id' => $this->id, - 'name' => $this->name, - 'expires_at' => $this->whenPivotLoaded('role_user', function () { - return $this->pivot->expires_at; - }), - ]; -} -``` - -If your intermediate table is using an accessor other than `pivot`, you may use the `whenPivotLoadedAs` method: - -```php - /** - * Transform the resource into an array. - * - * @return array - */ - public function toArray() - { - return [ - 'id' => $this->id, - 'name' => $this->name, - 'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () { - return $this->subscription->expires_at; - }), - ]; - } -``` - -### Adding Meta Data - -Some JSON API standards require the addition of meta data to your resource and resource collections responses. This often includes things like `links` to the resource or related resources, or meta data about the resource itself. If you need to return additional meta data about a resource, include it in your `toArray` method. For example, you might include `link` information when transforming a resource collection: - -```php -/** - * Transform the resource into an array. - * - * @return array - */ -public function toArray() -{ - return [ - 'data' => $this->collection, - 'links' => [ - 'self' => 'link-value', - ], - ]; -} -``` - -When returning additional meta data from your resources, you never have to worry about accidentally overriding the `links` or `meta` keys that are automatically added by Hypervel when returning paginated responses. Any additional `links` you define will be merged with the links provided by the paginator. - -#### Top Level Meta Data - -Sometimes you may wish to only include certain meta data with a resource response if the resource is the outermost resource being returned. Typically, this includes meta information about the response as a whole. To define this meta data, add a `with` method to your resource class. This method should return an array of meta data to be included with the resource response only when the resource is the outermost resource being rendered: - -```php - [ - 'key' => 'value', - ], - ]; - } -} -``` - -#### Adding Meta Data When Constructing Resources - -You may also add top-level data when constructing resource instances in your route or controller. The `additional` method, which is available on all resources, accepts an array of data that should be added to the resource response: - -```php -return (new UserCollection(User::all()->load('roles'))) - ->additional(['meta' => [ - 'key' => 'value', - ]]); -``` -## Resource Responses - -As you have already read, resources may be returned directly from routes and controllers: - -```php -use App\Http\Resources\User as UserResource; -use App\User; - -Route::get('/user', function () { - return new UserResource(User::find(1)); -}); -``` - -However, sometimes you may need to customize the outgoing HTTP response before it is sent to the client. There are two ways to accomplish this. First, you may chain the `response` method onto the resource. This method will return an `Hyperf\HttpMessage\Base\Response` instance, allowing you full control of the response's headers: - -```php -use App\Http\Resources\User as UserResource; -use App\User; - -Route::get('/user', function () { - return (new UserResource(User::find(1))) - ->toResponse() - ->withHeader('X-Value', 'True'); -}); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/eloquent-serialization.md b/_archive/docs/0.3/docs/eloquent-serialization.md deleted file mode 100644 index cf1f6e8a81..0000000000 --- a/_archive/docs/0.3/docs/eloquent-serialization.md +++ /dev/null @@ -1,188 +0,0 @@ -# Eloquent: Serialization -[[toc]] - -## Introduction - -When building JSON APIs, you will often need to convert your models and relationships to arrays or JSON. Eloquent includes convenient methods for making these conversions, as well as controlling which attributes are included in your serializations. - -## Serializing Models & Collections - -### Serializing To Arrays - -To convert a model and its loaded [relationships](/docs/eloquent-relationships) to an array, you should use the `toArray` method. This method is recursive, so all attributes and all relations (including the relations of relations) will be converted to arrays: - -```php -$user = App\User::with('roles')->first(); - -return $user->toArray(); -``` - -To convert only a model's attributes to an array, use the `attributesToArray` method: - -```php -$user = App\User::first(); - -return $user->attributesToArray(); -``` - -You may also convert entire [collections](/docs/eloquent-collections) of models to arrays: - -```php -$users = App\User::all(); - -return $users->toArray(); -``` - -### Serializing To JSON - -To convert a model to JSON, you should use the `toJson` method. Like `toArray`, the `toJson` method is recursive, so all attributes and relations will be converted to JSON. You may also specify JSON encoding options [supported by PHP](https://secure.php.net/manual/en/function.json-encode.php): - -```php -$user = App\User::find(1); - -return $user->toJson(); - -return $user->toJson(JSON_PRETTY_PRINT); -``` - -Alternatively, you may cast a model or collection to a string, which will automatically call the `toJson` method on the model or collection: - -```php -$user = App\User::find(1); - -return (string) $user; -``` - -Since models and collections are converted to JSON when cast to a string, you can return Eloquent objects directly from your application's routes or controllers: - -```php -Route::get('users', function () { - return App\User::all(); -}); -``` - -#### Relationships - -When an Eloquent model is converted to JSON, its loaded relationships will automatically be included as attributes on the JSON object. Also, though Eloquent relationship methods are defined using "camel case", a relationship's JSON attribute will be "snake case". - -## Hiding Attributes From JSON - -Sometimes you may wish to limit the attributes, such as passwords, that are included in your model's array or JSON representation. To do so, add a `$hidden` property to your model: - -```php -makeVisible('attribute')->toArray(); -``` - -Likewise, if you would like to make some typically visible attributes hidden on a given model instance, you may use the `makeHidden` method. - -```php -return $user->makeHidden('attribute')->toArray(); -``` - -## Appending Values To JSON - -Occasionally, when casting models to an array or JSON, you may wish to add attributes that do not have a corresponding column in your database. To do so, first define an [accessor](/docs/eloquent-mutators) for the value: - -```php -attributes['admin'] === 'yes'; - } -} -``` - -After creating the accessor, add the attribute name to the `appends` property on the model. Note that attribute names are typically referenced in "snake case", even though the accessor is defined using "camel case": - -```php -append('is_admin')->toArray(); - -return $user->setAppends(['is_admin'])->toArray(); -``` - -## Date Serialization - -#### Customizing The Date Format Per Attribute - -You may customize the serialization format of individual Eloquent date attributes by specifying the date format in the [cast declaration](/docs/eloquent-mutators#attribute-casting): - -```php -protected array $casts = [ - 'birthday' => 'date:Y-m-d', - 'joined_at' => 'datetime:Y-m-d H:00', -]; -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/eloquent.md b/_archive/docs/0.3/docs/eloquent.md deleted file mode 100644 index 4fabdca6ae..0000000000 --- a/_archive/docs/0.3/docs/eloquent.md +++ /dev/null @@ -1,1536 +0,0 @@ -# Eloquent: Getting Started -[[toc]] - -## Introduction - -Hypervel includes Eloquent, an object-relational mapper (ORM) that makes it enjoyable to interact with your database. When using Eloquent, each database table has a corresponding "Model" that is used to interact with that table. In addition to retrieving records from the database table, Eloquent models allow you to insert, update, and delete records from the table as well. - -::: note -Before getting started, be sure to configure a database connection in your application's `config/database.php` configuration file. For more information on configuring your database, check out [the database configuration documentation](/docs/database#configuration). -::: - -::: note -Hypervel's Eloquent is a forked version in the Hyperf framework, which was derived from an early version of Laravel. Some newer Eloquent features from recent Laravel versions may not be supported in this component. -::: - -## Generating Model Classes - -To get started, let's create an Eloquent model. Models typically live in the `app\Models` directory and extend the `Hypervel\Database\Eloquent\Model` class. You may use the `make:model` [Artisan command](/docs/artisan) to generate a new model: - -```shell:no-line-numbers -php artisan make:model Flight -``` - -If you would like to generate a [database migration](/docs/migrations) when you generate the model, you may use the `--migration` or `-m` option: - -```shell:no-line-numbers -php artisan make:model Flight --migration -``` - -You may generate various other types of classes when generating a model, such as factories, seeders, policies, controllers, and form requests. In addition, these options may be combined to create multiple classes at once: - -```shell:no-line-numbers -# Generate a model and a FlightFactory class... -php artisan make:model Flight --factory -php artisan make:model Flight -f - -# Generate a model and a FlightSeeder class... -php artisan make:model Flight --seed -php artisan make:model Flight -s - -# Shortcut to generate a model, migration, factory, seeder, policy, controller, and form requests... -php artisan make:model Flight --all -php artisan make:model Flight -a -``` - -## Eloquent Model Conventions - -Models generated by the `make:model` command will be placed in the `app/Models` directory. Let's examine a basic model class and discuss some of Eloquent's key conventions: - -```php - 'Traveling to Europe']); - -$article->id; // "8f8e8478-9035-4d23-b9a7-62f4d2612ce5" -``` - -By default, The `HasUuids` trait will generate ["ordered" UUIDs](/docs/strings#method-str-ordered-uuid) for your models. These UUIDs are more efficient for indexed database storage because they can be sorted lexicographically. - -You can override the UUID generation process for a given model by defining a `newUniqueId` method on the model. In addition, you may specify which columns should receive UUIDs by defining a `uniqueIds` method on the model: - -```php -use Ramsey\Uuid\Uuid; - -/** - * Generate a new UUID for the model. - */ -public function newUniqueId(): string -{ - return (string) Uuid::uuid4(); -} - -/** - * Get the columns that should receive a unique identifier. - * - * @return array - */ -public function uniqueIds(): array -{ - return ['id', 'discount_code']; -} -``` - -If you wish, you may choose to utilize "ULIDs" instead of UUIDs. ULIDs are similar to UUIDs; however, they are only 26 characters in length. Like ordered UUIDs, ULIDs are lexicographically sortable for efficient database indexing. To utilize ULIDs, you should use the `Hypervel\Database\Eloquent\Concerns\HasUlids` trait on your model. You should also ensure that the model has a [ULID equivalent primary key column](/docs/migrations#column-method-ulid): - -```php -use Hypervel\Database\Eloquent\Concerns\HasUlids; -use Hypervel\Database\Eloquent\Model; - -class Article extends Model -{ - use HasUlids; - - // ... -} - -$article = Article::create(['title' => 'Traveling to Asia']); - -$article->id; // "01gd4d3tgrrfqeda94gdbtdk5c" -``` - -::: tip -If you have `creating` hooks defined in the models, you need to call `creating` function from the parent class as well. - -```php -public function creating(): void -{ - parent::creating(); - - // your hook logic... -} -``` -::: - -### Timestamps - -By default, Eloquent expects `created_at` and `updated_at` columns to exist on your model's corresponding database table. Eloquent will automatically set these column's values when models are created or updated. If you do not want these columns to be automatically managed by Eloquent, you should define a `$timestamps` property on your model with a value of `false`: - -```php - $post->increment('reads')); -``` - -### Database Connections - -By default, all Eloquent models will use the default database connection that is configured for your application. If you would like to specify a different connection that should be used when interacting with a particular model, you should define a `$connection` property on the model: - -```php - '[]', - 'delayed' => false, - ]; -} -``` - -## Retrieving Models - -Once you have created a model and [its associated database table](/docs/migrations#generating-migrations), you are ready to start retrieving data from your database. You can think of each Eloquent model as a powerful [query builder](/docs/queries) allowing you to fluently query the database table associated with the model. The model's `all` method will retrieve all of the records from the model's associated database table: - -```php -use App\Models\Flight; - -foreach (Flight::all() as $flight) { - echo $flight->name; -} -``` - -#### Building Queries - -The Eloquent `all` method will return all of the results in the model's table. However, since each Eloquent model serves as a [query builder](/docs/queries), you may add additional constraints to queries and then invoke the `get` method to retrieve the results: - -```php -$flights = Flight::where('active', 1) - ->orderBy('name') - ->take(10) - ->get(); -``` - -::: note -Since Eloquent models are query builders, you should review all of the methods provided by Hypervel's [query builder](/docs/queries). You may use any of these methods when writing your Eloquent queries. -::: - -#### Refreshing Models - -If you already have an instance of an Eloquent model that was retrieved from the database, you can "refresh" the model using the `fresh` and `refresh` methods. The `fresh` method will re-retrieve the model from the database. The existing model instance will not be affected: - -```php -$flight = Flight::where('number', 'FR 900')->first(); - -$freshFlight = $flight->fresh(); -``` - -The `refresh` method will re-hydrate the existing model using fresh data from the database. In addition, all of its loaded relationships will be refreshed as well: - -```php -$flight = Flight::where('number', 'FR 900')->first(); - -$flight->number = 'FR 456'; - -$flight->refresh(); - -$flight->number; // "FR 900" -``` - -### Collections - -As we have seen, Eloquent methods like `all` and `get` retrieve multiple records from the database. However, these methods don't return a plain PHP array. Instead, an instance of `Hypervel\Database\Eloquent\Collection` is returned. - -The Eloquent `Collection` class extends Hypervel's base `Hypervel\Support\Collection` class, which provides a [variety of helpful methods](/docs/collections#available-methods) for interacting with data collections. For example, the `reject` method may be used to remove models from a collection based on the results of an invoked closure: - -```php -$flights = Flight::where('destination', 'Paris')->get(); - -$flights = $flights->reject(function (Flight $flight) { - return $flight->cancelled; -}); -``` - -In addition to the methods provided by Hypervel's base collection class, the Eloquent collection class provides [a few extra methods](/docs/eloquent-collections#available-methods) that are specifically intended for interacting with collections of Eloquent models. - -Since all of Hypervel's collections implement PHP's iterable interfaces, you may loop over collections as if they were an array: - -```php -foreach ($flights as $flight) { - echo $flight->name; -} -``` - -### Chunking Results - -Your application may run out of memory if you attempt to load tens of thousands of Eloquent records via the `all` or `get` methods. Instead of using these methods, the `chunk` method may be used to process large numbers of models more efficiently. - -The `chunk` method will retrieve a subset of Eloquent models, passing them to a closure for processing. Since only the current chunk of Eloquent models is retrieved at a time, the `chunk` method will provide significantly reduced memory usage when working with a large number of models: - -```php -use App\Models\Flight; -use Hypervel\Database\Eloquent\Collection; - -Flight::chunk(200, function (Collection $flights) { - foreach ($flights as $flight) { - // ... - } -}); -``` - -The first argument passed to the `chunk` method is the number of records you wish to receive per "chunk". The closure passed as the second argument will be invoked for each chunk that is retrieved from the database. A database query will be executed to retrieve each chunk of records passed to the closure. - -If you are filtering the results of the `chunk` method based on a column that you will also be updating while iterating over the results, you should use the `chunkById` method. Using the `chunk` method in these scenarios could lead to unexpected and inconsistent results. Internally, the `chunkById` method will always retrieve models with an `id` column greater than the last model in the previous chunk: - -```php -Flight::where('departed', true) - ->chunkById(200, function (Collection $flights) { - $flights->each->update(['departed' => false]); - }, column: 'id'); -``` - -Since the `chunkById` and `lazyById` methods add their own "where" conditions to the query being executed, you should typically [logically group](/docs/queries#logical-grouping) your own conditions within a closure: - -```php -Flight::where(function ($query) { - $query->where('delayed', true)->orWhere('cancelled', true); -})->chunkById(200, function (Collection $flights) { - $flights->each->update([ - 'departed' => false, - 'cancelled' => true - ]); -}, column: 'id'); -``` - -### Chunking Using Lazy Collections - -The `lazy` method works similarly to [the `chunk` method](#chunking-results) in the sense that, behind the scenes, it executes the query in chunks. However, instead of passing each chunk directly into a callback as is, the `lazy` method returns a flattened [`LazyCollection`](/docs/collections#lazy-collections) of Eloquent models, which lets you interact with the results as a single stream: - -```php -use App\Models\Flight; - -foreach (Flight::lazy() as $flight) { - // ... -} -``` - -If you are filtering the results of the `lazy` method based on a column that you will also be updating while iterating over the results, you should use the `lazyById` method. Internally, the `lazyById` method will always retrieve models with an `id` column greater than the last model in the previous chunk: - -```php -Flight::where('departed', true) - ->lazyById(200, column: 'id') - ->each->update(['departed' => false]); -``` - -You may filter the results based on the descending order of the `id` using the `lazyByIdDesc` method. - -### Cursors - -Similar to the `lazy` method, the `cursor` method may be used to significantly reduce your application's memory consumption when iterating through tens of thousands of Eloquent model records. - -The `cursor` method will only execute a single database query; however, the individual Eloquent models will not be hydrated until they are actually iterated over. Therefore, only one Eloquent model is kept in memory at any given time while iterating over the cursor. - -::: warning -Since the `cursor` method only ever holds a single Eloquent model in memory at a time, it cannot eager load relationships. If you need to eager load relationships, consider using [the `lazy` method](#chunking-using-lazy-collections) instead. -::: - -Internally, the `cursor` method uses PHP [generators](https://www.php.net/manual/en/language.generators.overview.php) to implement this functionality: - -```php -use App\Models\Flight; - -foreach (Flight::where('destination', 'Zurich')->cursor() as $flight) { - // ... -} -``` - -The `cursor` returns an `Hyperf\Collection\LazyCollection` instance. [Lazy collections](/docs/collections#lazy-collections) allow you to use many of the collection methods available on typical Hypervel collections while only loading a single model into memory at a time: - -```php -use App\Models\User; - -$users = User::cursor()->filter(function (User $user) { - return $user->id > 500; -}); - -foreach ($users as $user) { - echo $user->id; -} -``` - -Although the `cursor` method uses far less memory than a regular query (by only holding a single Eloquent model in memory at a time), it will still eventually run out of memory. This is [due to PHP's PDO driver internally caching all raw query results in its buffer](https://www.php.net/manual/en/mysqlinfo.concepts.buffering.php). If you're dealing with a very large number of Eloquent records, consider using [the `lazy` method](#chunking-using-lazy-collections) instead. - -### Advanced Subqueries - -#### Subquery Selects - -Eloquent also offers advanced subquery support, which allows you to pull information from related tables in a single query. For example, let's imagine that we have a table of flight `destinations` and a table of `flights` to destinations. The `flights` table contains an `arrived_at` column which indicates when the flight arrived at the destination. - -Using the subquery functionality available to the query builder's `select` and `addSelect` methods, we can select all of the `destinations` and the name of the flight that most recently arrived at that destination using a single query: - -```php -use App\Models\Destination; -use App\Models\Flight; - -return Destination::addSelect(['last_flight' => Flight::select('name') - ->whereColumn('destination_id', 'destinations.id') - ->orderByDesc('arrived_at') - ->limit(1) -])->get(); -``` - -#### Subquery Ordering - -In addition, the query builder's `orderBy` function supports subqueries. Continuing to use our flight example, we may use this functionality to sort all destinations based on when the last flight arrived at that destination. Again, this may be done while executing a single database query: - -```php -return Destination::orderByDesc( - Flight::select('arrived_at') - ->whereColumn('destination_id', 'destinations.id') - ->orderByDesc('arrived_at') - ->limit(1) -)->get(); -``` - -## Retrieving Single Models / Aggregates - -In addition to retrieving all of the records matching a given query, you may also retrieve single records using the `find`, `first`, or `firstWhere` methods. Instead of returning a collection of models, these methods return a single model instance: - -```php -use App\Models\Flight; - -// Retrieve a model by its primary key... -$flight = Flight::find(1); - -// Retrieve the first model matching the query constraints... -$flight = Flight::where('active', 1)->first(); - -// Alternative to retrieving the first model matching the query constraints... -$flight = Flight::firstWhere('active', 1); -``` - -Sometimes you may wish to perform some other action if no results are found. The `findOr` and `firstOr` methods will return a single model instance or, if no results are found, execute the given closure. The value returned by the closure will be considered the result of the method: - -```php -$flight = Flight::findOr(1, function () { - // ... -}); - -$flight = Flight::where('legs', '>', 3)->firstOr(function () { - // ... -}); -``` - -#### Not Found Exceptions - -Sometimes you may wish to throw an exception if a model is not found. This is particularly useful in routes or controllers. The `findOrFail` and `firstOrFail` methods will retrieve the first result of the query; however, if no result is found, an `Hypervel\Database\Eloquent\ModelNotFoundException` will be thrown: - -```php -$flight = Flight::findOrFail(1); - -$flight = Flight::where('legs', '>', 3)->firstOrFail(); -``` - -If the `ModelNotFoundException` is not caught, a 404 HTTP response is automatically sent back to the client: - -```php -use App\Models\Flight; - -Route::get('/api/flights/{id}', function (string $id) { - return Flight::findOrFail($id); -}); -``` - -### Retrieving or Creating Models - -The `firstOrCreate` method will attempt to locate a database record using the given column / value pairs. If the model cannot be found in the database, a record will be inserted with the attributes resulting from merging the first array argument with the optional second array argument: - -The `firstOrNew` method, like `firstOrCreate`, will attempt to locate a record in the database matching the given attributes. However, if a model is not found, a new model instance will be returned. Note that the model returned by `firstOrNew` has not yet been persisted to the database. You will need to manually call the `save` method to persist it: - -```php -use App\Models\Flight; - -// Retrieve flight by name or create it if it doesn't exist... -$flight = Flight::firstOrCreate([ - 'name' => 'London to Paris' -]); - -// Retrieve flight by name or create it with the name, delayed, and arrival_time attributes... -$flight = Flight::firstOrCreate( - ['name' => 'London to Paris'], - ['delayed' => 1, 'arrival_time' => '11:30'] -); - -// Retrieve flight by name or instantiate a new Flight instance... -$flight = Flight::firstOrNew([ - 'name' => 'London to Paris' -]); - -// Retrieve flight by name or instantiate with the name, delayed, and arrival_time attributes... -$flight = Flight::firstOrNew( - ['name' => 'Tokyo to Sydney'], - ['delayed' => 1, 'arrival_time' => '11:30'] -); -``` - -### Retrieving Aggregates - -When interacting with Eloquent models, you may also use the `count`, `sum`, `max`, and other [aggregate methods](/docs/queries#aggregates) provided by the Hypervel [query builder](/docs/queries). As you might expect, these methods return a scalar value instead of an Eloquent model instance: - -```php -$count = Flight::where('active', 1)->count(); - -$max = Flight::where('active', 1)->max('price'); -``` - -## Inserting and Updating Models - -### Inserts - -Of course, when using Eloquent, we don't only need to retrieve models from the database. We also need to insert new records. Thankfully, Eloquent makes it simple. To insert a new record into the database, you should instantiate a new model instance and set attributes on the model. Then, call the `save` method on the model instance: - -```php -name = $request->name; - - $flight->save(); - - return redirect('/flights'); - } -} -``` - -In this example, we assign the `name` field from the incoming HTTP request to the `name` attribute of the `App\Models\Flight` model instance. When we call the `save` method, a record will be inserted into the database. The model's `created_at` and `updated_at` timestamps will automatically be set when the `save` method is called, so there is no need to set them manually. - -Alternatively, you may use the `create` method to "save" a new model using a single PHP statement. The inserted model instance will be returned to you by the `create` method: - -```php -use App\Models\Flight; - -$flight = Flight::create([ - 'name' => 'London to Paris', -]); -``` - -However, before using the `create` method, you will need to specify either a `fillable` or `guarded` property on your model class. These properties are required because all Eloquent models are protected against mass assignment vulnerabilities by default. To learn more about mass assignment, please consult the [mass assignment documentation](#mass-assignment). - -### Updates - -The `save` method may also be used to update models that already exist in the database. To update a model, you should retrieve it and set any attributes you wish to update. Then, you should call the model's `save` method. Again, the `updated_at` timestamp will automatically be updated, so there is no need to manually set its value: - -```php -use App\Models\Flight; - -$flight = Flight::find(1); - -$flight->name = 'Paris to London'; - -$flight->save(); -``` - -Occasionally, you may need to update an existing model or create a new model if no matching model exists. Like the `firstOrCreate` method, the `updateOrCreate` method persists the model, so there's no need to manually call the `save` method. - -In the example below, if a flight exists with a `departure` location of `Oakland` and a `destination` location of `San Diego`, its `price` and `discounted` columns will be updated. If no such flight exists, a new flight will be created which has the attributes resulting from merging the first argument array with the second argument array: - -```php -$flight = Flight::updateOrCreate( - ['departure' => 'Oakland', 'destination' => 'San Diego'], - ['price' => 99, 'discounted' => 1] -); -``` - -#### Mass Updates - -Updates can also be performed against models that match a given query. In this example, all flights that are `active` and have a `destination` of `San Diego` will be marked as delayed: - -```php -Flight::where('active', 1) - ->where('destination', 'San Diego') - ->update(['delayed' => 1]); -``` - -The `update` method expects an array of column and value pairs representing the columns that should be updated. The `update` method returns the number of affected rows. - -::: warning -When issuing a mass update via Eloquent, the `saving`, `saved`, `updating`, and `updated` model events will not be fired for the updated models. This is because the models are never actually retrieved when issuing a mass update. -::: - -#### Examining Attribute Changes - -Eloquent provides the `isDirty`, `isClean`, and `wasChanged` methods to examine the internal state of your model and determine how its attributes have changed from when the model was originally retrieved. - -The `isDirty` method determines if any of the model's attributes have been changed since the model was retrieved. You may pass a specific attribute name or an array of attributes to the `isDirty` method to determine if any of the attributes are "dirty". The `isClean` method will determine if an attribute has remained unchanged since the model was retrieved. This method also accepts an optional attribute argument: - -```php -use App\Models\User; - -$user = User::create([ - 'first_name' => 'Taylor', - 'last_name' => 'Otwell', - 'title' => 'Developer', -]); - -$user->title = 'Painter'; - -$user->isDirty(); // true -$user->isDirty('title'); // true -$user->isDirty('first_name'); // false -$user->isDirty(['first_name', 'title']); // true - -$user->isClean(); // false -$user->isClean('title'); // false -$user->isClean('first_name'); // true -$user->isClean(['first_name', 'title']); // false - -$user->save(); - -$user->isDirty(); // false -$user->isClean(); // true -``` - -The `wasChanged` method determines if any attributes were changed when the model was last saved within the current request cycle. If needed, you may pass an attribute name to see if a particular attribute was changed: - -```php -$user = User::create([ - 'first_name' => 'Taylor', - 'last_name' => 'Otwell', - 'title' => 'Developer', -]); - -$user->title = 'Painter'; - -$user->save(); - -$user->wasChanged(); // true -$user->wasChanged('title'); // true -$user->wasChanged(['title', 'slug']); // true -$user->wasChanged('first_name'); // false -$user->wasChanged(['first_name', 'title']); // true -``` - -The `getOriginal` method returns an array containing the original attributes of the model regardless of any changes to the model since it was retrieved. If needed, you may pass a specific attribute name to get the original value of a particular attribute: - -```php -$user = User::find(1); - -$user->name; // John -$user->email; // john@example.com - -$user->name = "Jack"; -$user->name; // Jack - -$user->getOriginal('name'); // John -$user->getOriginal(); // Array of original attributes... -``` - -### Mass Assignment - -You may use the `create` method to "save" a new model using a single PHP statement. The inserted model instance will be returned to you by the method: - -```php -use App\Models\Flight; - -$flight = Flight::create([ - 'name' => 'London to Paris', -]); -``` - -However, before using the `create` method, you will need to specify either a `fillable` or `guarded` property on your model class. These properties are required because all Eloquent models are protected against mass assignment vulnerabilities by default. - -A mass assignment vulnerability occurs when a user passes an unexpected HTTP request field and that field changes a column in your database that you did not expect. For example, a malicious user might send an `is_admin` parameter through an HTTP request, which is then passed to your model's `create` method, allowing the user to escalate themselves to an administrator. - -So, to get started, you should define which model attributes you want to make mass assignable. You may do this using the `$fillable` property on the model. For example, let's make the `name` attribute of our `Flight` model mass assignable: - -```php - 'London to Paris']); -``` - -If you already have a model instance, you may use the `fill` method to populate it with an array of attributes: - -```php -$flight->fill(['name' => 'Amsterdam to Frankfurt']); -``` - -#### Mass Assignment and JSON Columns - -When assigning JSON columns, each column's mass assignable key must be specified in your model's `$fillable` array. For security, Hypervel does not support updating nested JSON attributes when using the `guarded` property: - -```php -/** - * The attributes that are mass assignable. - */ -protected array $fillable = [ - 'options->enabled', -]; -``` - -#### Allowing Mass Assignment - -If you would like to make all of your attributes mass assignable, you may define your model's `$guarded` property as an empty array. If you choose to unguard your model, you should take special care to always hand-craft the arrays passed to Eloquent's `fill`, `create`, and `update` methods: - -```php -/** - * The attributes that aren't mass assignable. - */ -protected array $guarded = []; -``` - -#### Mass Assignment Exceptions - -By default, attributes that are not included in the `$fillable` array are silently discarded when performing mass-assignment operations. In production, this is expected behavior; however, during local development it can lead to confusion as to why model changes are not taking effect. - -If you wish, you may instruct Hypervel to throw an exception when attempting to fill an unfillable attribute by invoking the `preventSilentlyDiscardingAttributes` method. Typically, this method should be invoked in the `boot` method of your application's `AppServiceProvider` class: - -```php -use Hypervel\Database\Eloquent\Model; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Model::preventSilentlyDiscardingAttributes($this->app->isLocal()); -} -``` - -### Upserts - -Eloquent's `upsert` method may be used to update or create records in a single, atomic operation. The method's first argument consists of the values to insert or update, while the second argument lists the column(s) that uniquely identify records within the associated table. The method's third and final argument is an array of the columns that should be updated if a matching record already exists in the database. The `upsert` method will automatically set the `created_at` and `updated_at` timestamps if timestamps are enabled on the model: - -```php -Flight::upsert([ - ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99], - ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150] -], uniqueBy: ['departure', 'destination'], update: ['price']); -``` - -::: warning -All databases except SQL Server require the columns in the second argument of the `upsert` method to have a "primary" or "unique" index. In addition, the MariaDB and MySQL database drivers ignore the second argument of the `upsert` method and always use the "primary" and "unique" indexes of the table to detect existing records. -::: - -## Deleting Models - -To delete a model, you may call the `delete` method on the model instance: - -```php -use App\Models\Flight; - -$flight = Flight::find(1); - -$flight->delete(); -``` - -#### Deleting an Existing Model by its Primary Key - -In the example above, we are retrieving the model from the database before calling the `delete` method. However, if you know the primary key of the model, you may delete the model without explicitly retrieving it by calling the `destroy` method. In addition to accepting the single primary key, the `destroy` method will accept multiple primary keys, an array of primary keys, or a [collection](/docs/collections) of primary keys: - -```php -Flight::destroy(1); - -Flight::destroy(1, 2, 3); - -Flight::destroy([1, 2, 3]); - -Flight::destroy(collect([1, 2, 3])); -``` - -::: warning -The `destroy` method loads each model individually and calls the `delete` method so that the `deleting` and `deleted` events are properly dispatched for each model. -::: - -#### Deleting Models Using Queries - -Of course, you may build an Eloquent query to delete all models matching your query's criteria. In this example, we will delete all flights that are marked as inactive. Like mass updates, mass deletes will not dispatch model events for the models that are deleted: - -```php -$deleted = Flight::where('active', 0)->delete(); -``` - -To delete all models in a table, you should execute a query without adding any conditions: - -```php -$deleted = Flight::query()->delete(); -``` - -::: warning -When executing a mass delete statement via Eloquent, the `deleting` and `deleted` model events will not be dispatched for the deleted models. This is because the models are never actually retrieved when executing the delete statement. -::: - -### Soft Deleting - -In addition to actually removing records from your database, Eloquent can also "soft delete" models. When models are soft deleted, they are not actually removed from your database. Instead, a `deleted_at` attribute is set on the model indicating the date and time at which the model was "deleted". To enable soft deletes for a model, add the `Hyperf\Database\Model\SoftDeletes` trait to the model: - -```php -softDeletes(); -}); - -Schema::table('flights', function (Blueprint $table) { - $table->dropSoftDeletes(); -}); -``` - -Now, when you call the `delete` method on the model, the `deleted_at` column will be set to the current date and time. However, the model's database record will be left in the table. When querying a model that uses soft deletes, the soft deleted models will automatically be excluded from all query results. - -To determine if a given model instance has been soft deleted, you may use the `trashed` method: - -```php -if ($flight->trashed()) { - // ... -} -``` - -#### Restoring Soft Deleted Models - -Sometimes you may wish to "un-delete" a soft deleted model. To restore a soft deleted model, you may call the `restore` method on a model instance. The `restore` method will set the model's `deleted_at` column to `null`: - -```php -$flight->restore(); -``` - -You may also use the `restore` method in a query to restore multiple models. Again, like other "mass" operations, this will not dispatch any model events for the models that are restored: - -```php -Flight::withTrashed() - ->where('airline_id', 1) - ->restore(); -``` - -The `restore` method may also be used when building [relationship](/docs/eloquent-relationships) queries: - -```php -$flight->history()->restore(); -``` - -#### Permanently Deleting Models - -Sometimes you may need to truly remove a model from your database. You may use the `forceDelete` method to permanently remove a soft deleted model from the database table: - -```php -$flight->forceDelete(); -``` - -You may also use the `forceDelete` method when building Eloquent relationship queries: - -```php -$flight->history()->forceDelete(); -``` - -### Querying Soft Deleted Models - -#### Including Soft Deleted Models - -As noted above, soft deleted models will automatically be excluded from query results. However, you may force soft deleted models to be included in a query's results by calling the `withTrashed` method on the query: - -```php -use App\Models\Flight; - -$flights = Flight::withTrashed() - ->where('account_id', 1) - ->get(); -``` - -The `withTrashed` method may also be called when building a [relationship](/docs/eloquent-relationships) query: - -```php -$flight->history()->withTrashed()->get(); -``` - -#### Retrieving Only Soft Deleted Models - -The `onlyTrashed` method will retrieve **only** soft deleted models: - -```php -$flights = Flight::onlyTrashed() - ->where('airline_id', 1) - ->get(); -``` - -## Replicating Models - -You may create an unsaved copy of an existing model instance using the `replicate` method. This method is particularly useful when you have model instances that share many of the same attributes: - -```php -use App\Models\Address; - -$shipping = Address::create([ - 'type' => 'shipping', - 'line_1' => '123 Example Street', - 'city' => 'Victorville', - 'state' => 'CA', - 'postcode' => '90001', -]); - -$billing = $shipping->replicate()->fill([ - 'type' => 'billing' -]); - -$billing->save(); -``` - -To exclude one or more attributes from being replicated to the new model, you may pass an array to the `replicate` method: - -```php -$flight = Flight::create([ - 'destination' => 'LAX', - 'origin' => 'LHR', - 'last_flown' => '2020-03-04 11:00:00', - 'last_pilot_id' => 747, -]); - -$flight = $flight->replicate([ - 'last_flown', - 'last_pilot_id' -]); -``` - -## Query Scopes - -### Global Scopes - -Global scopes allow you to add constraints to all queries for a given model. Hypervel's own [soft delete](#soft-deleting) functionality utilizes global scopes to only retrieve "non-deleted" models from the database. Writing your own global scopes can provide a convenient, easy way to make sure every query for a given model receives certain constraints. - -#### Generating Scopes - -To generate a new global scope, you may invoke the `make:scope` Artisan command, which will place the generated scope in your application's `app/Models/Scopes` directory: - -```shell:no-line-numbers -php artisan make:scope AncientScope -``` - -#### Writing Global Scopes - -Writing a global scope is simple. First, use the `make:scope` command to generate a class that implements the `Hyperf\Database\Model\Scope` interface. The `Scope` interface requires you to implement one method: `apply`. The `apply` method may add `where` constraints or other types of clauses to the query as needed: - -```php -where('created_at', '<', now()->subYears(2000)); - } -} -``` - -::: note -If your global scope is adding columns to the select clause of the query, you should use the `addSelect` method instead of `select`. This will prevent the unintentional replacement of the query's existing select clause. -::: - -#### Applying Global Scopes - -To assign a global scope to a model, you need to register the global scope by overriding the model's `booted` method and invoke the model's `addGlobalScope` method. The `addGlobalScope` method accepts an instance of your scope as its only argument: - -```php -where('created_at', '<', now()->subYears(2000)); - }); - } -} -``` - -#### Removing Global Scopes - -If you would like to remove a global scope for a given query, you may use the `withoutGlobalScope` method. This method accepts the class name of the global scope as its only argument: - -```php -User::withoutGlobalScope(AncientScope::class)->get(); -``` - -Or, if you defined the global scope using a closure, you should pass the string name that you assigned to the global scope: - -```php -User::withoutGlobalScope('ancient')->get(); -``` - -If you would like to remove several or even all of the query's global scopes, you may use the `withoutGlobalScopes` method: - -```php -// Remove all of the global scopes... -User::withoutGlobalScopes()->get(); - -// Remove some of the global scopes... -User::withoutGlobalScopes([ - FirstScope::class, SecondScope::class -])->get(); -``` - -### Local Scopes - -Local scopes allow you to define common sets of query constraints that you may easily re-use throughout your application. For example, you may need to frequently retrieve all users that are considered "popular". To define a scope, prefix an Eloquent model method with `scope`. - -Scopes should always return the same query builder instance or `void`: - -```php -where('votes', '>', 100); - } - - /** - * Scope a query to only include active users. - */ - public function scopeActive(Builder $query): void - { - $query->where('active', 1); - } -} -``` - -#### Utilizing a Local Scope - -Once the scope has been defined, you may call the scope methods when querying the model. However, you should not include the `scope` prefix when calling the method. You can even chain calls to various scopes: - -```php -use App\Models\User; - -$users = User::popular()->active()->orderBy('created_at')->get(); -``` - -Combining multiple Eloquent model scopes via an `or` query operator may require the use of closures to achieve the correct [logical grouping](/docs/queries#logical-grouping): - -```php -$users = User::popular()->orWhere(function (Builder $query) { - $query->active(); -})->get(); -``` - -However, since this can be cumbersome, Hypervel provides a "higher order" `orWhere` method that allows you to fluently chain scopes together without the use of closures: - -```php -$users = User::popular()->orWhere->active()->get(); -``` - -#### Dynamic Scopes - -Sometimes you may wish to define a scope that accepts parameters. To get started, just add your additional parameters to your scope method's signature. Scope parameters should be defined after the `$query` parameter: - -```php -where('type', $type); - } -} -``` - -Once the expected arguments have been added to your scope method's signature, you may pass the arguments when calling the scope: - -```php -$users = User::ofType('admin')->get(); -``` - -### Pending Attributes - -If you would like to use scopes to create models that have the same attributes as those used to constrain the scope, you may use the `withAttributes` method when building the scope query: - -```php -withAttributes([ - 'hidden' => true, - ]); - } -} -``` - -The `withAttributes` method will add `where` clause constraints to the query using the given attributes, and it will also add the given attributes to any models created via the scope: - -```php -$draft = Post::draft()->create(['title' => 'In Progress']); - -$draft->hidden; // true -``` - -## Comparing Models - -Sometimes you may need to determine if two models are the "same" or not. The `is` and `isNot` methods may be used to quickly verify two models have the same primary key, table, and database connection or not: - -```php -if ($post->is($anotherPost)) { - // ... -} - -if ($post->isNot($anotherPost)) { - // ... -} -``` - -The `is` and `isNot` methods are also available when using the `belongsTo`, `hasOne`, `morphTo`, and `morphOne` [relationships](/docs/eloquent-relationships). This method is particularly helpful when you would like to compare a related model without issuing a query to retrieve that model: - -```php -if ($post->author()->is($user)) { - // ... -} -``` - -## Events - -::: note -Want to broadcast your Eloquent events directly to your client-side application? Check out Hypervel's [model event broadcasting](/docs/broadcasting#model-broadcasting). -::: - -Eloquent models dispatch several events, allowing you to hook into the following moments in a model's lifecycle: `retrieved`, `creating`, `created`, `updating`, `updated`, `saving`, `saved`, `deleting`, `deleted`, `forceDeleting`, `forceDeleted`, `restoring` and `restored`. - -The `retrieved` event will dispatch when an existing model is retrieved from the database. When a new model is saved for the first time, the `creating` and `created` events will dispatch. The `updating` / `updated` events will dispatch when an existing model is modified and the `save` method is called. The `saving` / `saved` events will dispatch when a model is created or updated - even if the model's attributes have not been changed. Event names ending with `-ing` are dispatched before any changes to the model are persisted, while events ending with `-ed` are dispatched after the changes to the model are persisted. - -To start listening to model events, define related functions on your Eloquent model: - -```php -setCreatedAt('2025-01-01'); - } -} -``` -::: warning -When issuing a mass update or delete query via Eloquent, the `saved`, `updated`, `deleting`, and `deleted` model events will not be dispatched for the affected models. This is because the models are never actually retrieved when performing mass updates or deletes. -::: - -### Using Closures - -Instead of using custom event classes, you may register closures that execute when various model events are dispatched. Typically, you should register these closures in the `booted` method of your model: - -```php -delete(); - - return User::find(2); -}); -``` - -#### Saving a Single Model Without Events - -Sometimes you may wish to "save" a given model without dispatching any events. You may accomplish this using the `saveQuietly` method: - -```php -$user = User::findOrFail(1); - -$user->name = 'Victoria Faith'; - -$user->saveQuietly(); -``` - -You may also "update", "delete", "soft delete", "restore", and "replicate" a given model without dispatching any events: - -```php -$user->deleteQuietly(); -$user->forceDeleteQuietly(); -$user->restoreQuietly(); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/encryption.md b/_archive/docs/0.3/docs/encryption.md deleted file mode 100644 index c606081304..0000000000 --- a/_archive/docs/0.3/docs/encryption.md +++ /dev/null @@ -1,71 +0,0 @@ -# Encryption -[[toc]] - -## Introduction - -Hypervel's encryption services provide a simple, convenient interface for encrypting and decrypting text via OpenSSL using AES-256 and AES-128 encryption. All of Hypervel's encrypted values are signed using a message authentication code (MAC) so that their underlying value cannot be modified or tampered with once encrypted. - -## Configuration - -Before using Hypervel's encrypter, you must set the `key` configuration option in your `config/app.php` configuration file. This configuration value is driven by the `APP_KEY` environment variable. You should use the `php artisan key:generate` command to generate this variable's value since the `key:generate` command will use PHP's secure random bytes generator to build a cryptographically secure key for your application. Typically, the value of the `APP_KEY` environment variable will be generated for you during [Hypervel's installation](/docs/installation). - -### Gracefully Rotating Encryption Keys - -If you change your application's encryption key, all authenticated user sessions will be logged out of your application. This is because every cookie, including session cookies, are encrypted by Hypervel. In addition, it will no longer be possible to decrypt any data that was encrypted with your previous encryption key. - -To mitigate this issue, Hypervel allows you to list your previous encryption keys in your application's `APP_PREVIOUS_KEYS` environment variable. This variable may contain a comma-delimited list of all of your previous encryption keys: - -```ini -APP_KEY="base64:J63qRTDLub5NuZvP+kb8YIorGS6qFYHKVo6u7179stY=" -APP_PREVIOUS_KEYS="base64:2nLsGFGzyoae2ax3EF2Lyq/hH6QghBGLIq5uL+Gp8/w=" -``` - -When you set this environment variable, Hypervel will always use the "current" encryption key when encrypting values. However, when decrypting values, Hypervel will first try the current key, and if decryption fails using the current key, Hypervel will try all previous keys until one of the keys is able to decrypt the value. - -This approach to graceful decryption allows users to keep using your application uninterrupted even if your encryption key is rotated. - -## Using the Encrypter - -#### Encrypting a Value - -You may encrypt a value using the `encryptString` method provided by the `Crypt` facade. All encrypted values are encrypted using OpenSSL and the AES-256-CBC cipher. Furthermore, all encrypted values are signed with a message authentication code (MAC). The integrated message authentication code will prevent the decryption of any values that have been tampered with by malicious users: - -```php -user()->fill([ - 'token' => Crypt::encryptString($request->token), - ])->save(); - - return redirect('/secrets'); - } -} -``` - -#### Decrypting a Value - -You may decrypt values using the `decryptString` method provided by the `Crypt` facade. If the value cannot be properly decrypted, such as when the message authentication code is invalid, an `Hypervel\Encryption\Contracts\DecryptException` will be thrown: - -```php -use Hypervel\Encryption\Contracts\DecryptException; -use Hypervel\Support\Facades\Crypt; - -try { - $decrypted = Crypt::decryptString($encryptedValue); -} catch (DecryptException $e) { - // ... -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/errors.md b/_archive/docs/0.3/docs/errors.md deleted file mode 100644 index 7d70e2cd05..0000000000 --- a/_archive/docs/0.3/docs/errors.md +++ /dev/null @@ -1,413 +0,0 @@ -# Error Handling -[[toc]] - -## Introduction - -When you start a new Hypervel project, error and exception handling is already configured for you. The `App\Exceptions\Handler` class is where all exceptions thrown by your application are logged and then rendered to the user. We'll dive deeper into this class throughout this documentation. - -::: info -`App\Exceptions\Handler` is built on top of [Hyperf's ExceptionHandler](https://hyperf.wiki/#/en/exception-handler). Hypervel migrates the most of error handling features from Laravel. -::: - -## Configuration - -The `debug` option in your `config/app.php` configuration file determines how much information about an error is actually displayed to the user. By default, this option is set to respect the value of the `APP_DEBUG` environment variable, which is stored in your `.env` file. - -During local development, you should set the `APP_DEBUG` environment variable to `true`. **In your production environment, this value should always be `false`. If the value is set to `true` in production, you risk exposing sensitive configuration values to your application's end users.** - -::: warning -`report` feature replies on `report` method in `Hypervel\Foundation\Exceptions\Contracts\ExceptionHandler`, if you are using custom exception handler, you need to implement the `report` method by yourself. -::: - -## Handling Exceptions - -### Reporting Exceptions - -In Hypervel, exception reporting is used to log exceptions or send them to an external service. By default, exceptions will be logged based on your [logging](/docs/logging) configuration. However, you are free to log exceptions however you wish. - -If you need to report different types of exceptions in different ways, you may use the `reportable` method to register a closure that should be executed when an exception of a given type needs to be reported. Hypervel will determine what type of exception the closure reports by examining the type-hint of the closure: - -```php -use App\Exceptions\InvalidOrderException; - -/** - * Register the exception handling callbacks for the application. - */ -public function register(): void -{ - $this->reportable(function (InvalidOrderException $e) { - // ... - }); -} -``` - -When you register a custom exception reporting callback using the `reportable` method, Hypervel will still log the exception using the default logging configuration for the application. If you wish to stop the propagation of the exception to the default logging stack, you may use the `stop` method when defining your reporting callback or return `false` from the callback: - -```php -$this->reportable(function (InvalidOrderException $e) { - // ... -})->stop(); - -$this->reportable(function (InvalidOrderException $e) { - return false; -}); -``` - -::: note -To customize the exception reporting for a given exception, you may also utilize [reportable exceptions](/docs/errors#renderable-exceptions). -::: - -#### Global Log Context - -If available, Hypervel automatically adds the current user's ID to every exception's log message as contextual data. You may define your own global contextual data by defining a `context` method on your application's `App\Exceptions\Handler` class. This information will be included in every exception's log message written by your application: - -```php -/** - * Get the default context variables for logging. - * - * @return array - */ -protected function context(): array -{ - return array_merge(parent::context(), [ - 'foo' => 'bar', - ]); -} -``` - - -#### Exception Log Context - -While adding context to every log message can be useful, sometimes a particular exception may have unique context that you would like to include in your logs. By defining a `context` method on one of your application's exceptions, you may specify any data relevant to that exception that should be added to the exception's log entry: - -```php - - */ - public function context(): array - { - return ['order_id' => $this->orderId]; - } -} -``` - -#### The `report` Helper - -Sometimes you may need to report an exception but continue handling the current request. The `report` helper function allows you to quickly report an exception without rendering an error page to the user: - -```php -public function isValid(string $value): bool -{ - try { - // Validate the value... - } catch (Throwable $e) { - report($e); - - return false; - } -} -``` - -#### Deduplicating Reported Exceptions - -If you are using the `report` function throughout your application, you may occasionally report the same exception multiple times, creating duplicate entries in your logs. - -If you would like to ensure that a single instance of an exception is only ever reported once, you may set the `$withoutDuplicates` property to `true` within your application's `App\Exceptions\Handler` class: - -```php -namespace App\Exceptions; - -use Hypervel\Foundation\Exceptions\Handler as ExceptionHandler; - -class Handler extends ExceptionHandler -{ - /** - * Indicates that an exception instance should only be reported once. - */ - protected bool $withoutDuplicates = true; - - // ... -} -``` - -Now, when the `report` helper is called with the same instance of an exception, only the first call will be reported: - -```php -$original = new RuntimeException('Whoops!'); - -report($original); // reported - -try { - throw $original; -} catch (Throwable $caught) { - report($caught); // ignored -} - -report($original); // ignored -report($caught); // ignored -``` - -### Exception Log Levels - -When messages are written to your application's [logs](/docs/logging), the messages are written at a specified [log level](/docs/logging#log-levels), which indicates the severity or importance of the message being logged. - -As noted above, even when you register a custom exception reporting callback using the `reportable` method, Hypervel will still log the exception using the default logging configuration for the application; however, since the log level can sometimes influence the channels on which a message is logged, you may wish to configure the log level that certain exceptions are logged at. - -To accomplish this, you may define a `$levels` property on your application's exception handler. This property should contain an array of exception types and their associated log levels: - -```php -use PDOException; -use Psr\Log\LogLevel; - -/** - * A list of exception types with their corresponding custom log levels. - * - * @var array, \Psr\Log\LogLevel::*> - */ -protected array $levels = [ - PDOException::class => LogLevel::CRITICAL, -]; -``` - -### Ignoring Exceptions by Type - -When building your application, there will be some types of exceptions you never want to report. To ignore these exceptions, define a `$dontReport` property on your application's exception handler. Any classes that you add to this property will never be reported; however, they may still have custom rendering logic: - -```php -use App\Exceptions\InvalidOrderException; - -/** - * A list of the exception types that are not reported. - * - * @var array> - */ -protected array $dontReport = [ - InvalidOrderException::class, -]; -``` - -Internally, Hypervel already ignores some types of errors for you, such as exceptions resulting from 404 HTTP errors or 403 HTTP responses generated by auth exceptions. If you would like to instruct Hypervel to stop ignoring a given type of exception, you may invoke the `stopIgnoring` method within your exception handler's `register` method: - -```php -use Hypervel\HttpMessage\Exceptions\NotFoundHttpException; - -/** - * Register the exception handling callbacks for the application. - */ -public function register(): void -{ - $this->stopIgnoring(HttpException::class); - - // ... -} -``` - -### Rendering Exceptions - -By default, the Hypervel exception handler will convert exceptions into an HTTP response for you. However, you are free to register a custom rendering closure for exceptions of a given type. You may accomplish this by invoking the `renderable` method within your exception handler. - -The closure passed to the `renderable` method should return an instance of `Psr\Http\Message\ResponseInterface`, which may be generated via the `response` helper. Hypervel will determine what type of exception the closure renders by examining the type-hint of the closure: - -```php -use App\Exceptions\InvalidOrderException; -use Hypervel\Http\Request; - -/** - * Register the exception handling callbacks for the application. - */ -public function register(): void -{ - $this->renderable(function (InvalidOrderException $e, Request $request) { - return response()->view('errors.invalid-order', [], 500); - }); -} -``` - -You may also use the `renderable` method to override the rendering behavior for built-in Hypervel exceptions such as `NotFoundHttpException`. If the closure given to the `renderable` method does not return a value, Hypervel's default exception rendering will be utilized: - -```php -use Hypervel\Http\Request; -use Hypervel\HttpMessage\Exceptions\NotFoundHttpException; - -/** - * Register the exception handling callbacks for the application. - */ -public function register(): void -{ - $this->renderable(function (NotFoundHttpException $e, Request $request) { - if ($request->is('api/*')) { - return response()->json([ - 'message' => 'Record not found.' - ], 404); - } - }); -} -``` - -#### Rendering Exceptions as JSON - -When rendering an exception, Hypervel will automatically determine if the exception should be rendered as an HTML or JSON response based on the `Accept` header of the request. If you would like to customize how Hypervel determines whether to render HTML or JSON exception responses, you may utilize the `shouldRenderJsonWhen` method: - -```php -use Hypervel\Http\Request; -use Hypervel\HttpMessage\Exceptions\NotFoundHttpException; - -/** - * Register the exception handling callbacks for the application. - */ -public function register(): void -{ - $this->shouldRenderJsonWhen(function (Request $request, Throwable $e) { - return $request->is('admin/*'); - }); -} -``` - -#### Customizing the Exception Response - -Rarely, you may need to customize the entire HTTP response rendered by Hypervel's exception handler. To accomplish this, you may register a response customization closure using the `respondUsing` method: - -```php -use Psr\Http\Message\ResponseInterface; -use Hypervel\Http\Request; -use Hypervel\HttpMessage\Exceptions\NotFoundHttpException; - -/** - * Register the exception handling callbacks for the application. - */ -public function register(): void -{ - $this->respondUsing(function (ResponseInterface $response, Throwable $e, Request $request) { - if ($response->getStatusCode() === 419) { - return response()->json([ - 'code' => 419, - 'message' => 'The page expired, please try again.', - ]); - } - - return $response; - }); -} -``` - -### Reportable and Renderable Exceptions - -Instead of defining custom reporting and rendering behavior in your exception handler's `register` method, you may define `report` and `render` methods directly on your application's exceptions. When these methods exist, they will automatically be called by the framework: - -```php -{{ $exception->getMessage() }} -``` - -#### Fallback HTTP Error Pages - -You may also define a "fallback" error page for a given series of HTTP status codes. This page will be rendered if there is not a corresponding page for the specific HTTP status code that occurred. To accomplish this, define a `4xx.blade.php` template and a `5xx.blade.php` template in your application's `resources/views/errors` directory. - -## Whoops Exception Renderer - -Hypervel has a simple built-in HTML error page renderer for debugging. You can choose to use [Whoops](https://github.com/filp/whoops) as an alternative renderer. - -![Whoops](/images/whoops.png) - -Before enabling whoops error handler, make sure you've installed `filp/whoops` package in your project, and bind Whoops renderer in the service provider. - -```php -$this->app->bind( - \Hypervel\Foundation\Exceptions\Contracts\ExceptionRenderer::class, - \Hypervel\Foundation\Exceptions\WhoopsErrorRenderer::class -); -``` - -You can also implement your own renderer by implementing the `Hypervel\Foundation\Exceptions\Contracts\ExceptionRenderer` contract. \ No newline at end of file diff --git a/_archive/docs/0.3/docs/events.md b/_archive/docs/0.3/docs/events.md deleted file mode 100644 index a695d758ea..0000000000 --- a/_archive/docs/0.3/docs/events.md +++ /dev/null @@ -1,636 +0,0 @@ -## Introduction - -Hypervel's events provide a simple observer pattern implementation, allowing you to subscribe and listen for various events that occur within your application. Event classes are typically stored in the `app/Events` directory, while their listeners are stored in `app/Listeners`. Don't worry if you don't see these directories in your application as they will be created for you as you generate events and listeners using Artisan console commands. - -Events serve as a great way to decouple various aspects of your application, since a single event can have multiple listeners that do not depend on each other. For example, you may wish to send a Slack notification to your user each time an order has shipped. Instead of coupling your order processing code to your Slack notification code, you can raise an `App\Events\OrderShipped` event which a listener can receive and use to dispatch a Slack notification. - -::: note -Hypervel's events provide similar functionality to Laravel's events. To keep compatibility with Hyperf's events, events in Hypervel is built on Hyperf's event component. -::: - -## Registering Events and Listeners - -The `App\Providers\EventServiceProvider` included with your Hypervel application provides a convenient place to register all of your application's event listeners. The `listen` property contains an array of all events (keys) and their listeners (values). You may add as many events to this array as your application requires. For example, let's add an `OrderShipped` event: - -```php -use App\Events\OrderShipped; -use App\Listeners\SendShipmentNotification; - -/** - * The event listener mappings for the application. - * - * @var array> - */ -protected array $listen = [ - OrderShipped::class => [ - SendShipmentNotification::class, - ], -]; -``` - -::: note -The `event:list` command may be used to display a list of all events and listeners registered by your application. -::: - -### Generating Events and Listeners - -You may use the `make:event` and `make:listener` Artisan commands to generate individual events and listeners: - -```shell -php artisan make:event PodcastProcessed - -php artisan make:listener SendPodcastNotification -``` - -### Manually Registering Events - -Typically, events should be registered via the `EventServiceProvider` `$listen` array; however, you may also register class or closure based event listeners manually in the `boot` method of your `EventServiceProvider`: - -```php -use App\Events\PodcastProcessed; -use App\Listeners\SendPodcastNotification; -use Hypervel\Support\Facades\Event; - -/** - * Register any other events for your application. - */ -public function boot(): void -{ - Event::listen( - PodcastProcessed::class, - SendPodcastNotification::class, - ); - - Event::listen(function (PodcastProcessed $event) { - // ... - }); -} -``` - -#### Queueable Anonymous Event Listeners - -When registering closure based event listeners manually, you may wrap the listener closure within the `Hypervel\Event\queueable` function to instruct Hypervel to execute the listener using the [queue](/docs/queues): - -```php -use App\Events\PodcastProcessed; -use function Hypervel\Events\queueable; -use Hypervel\Support\Facades\Event; - -/** - * Register any other events for your application. - */ -public function boot(): void -{ - Event::listen(queueable(function (PodcastProcessed $event) { - // ... - })); -} -``` - -Like queued jobs, you may use the `onConnection`, `onQueue`, and `delay` methods to customize the execution of the queued listener: - -```php -Event::listen(queueable(function (PodcastProcessed $event) { - // ... -})->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10))); -``` - -If you would like to handle anonymous queued listener failures, you may provide a closure to the `catch` method while defining the `queueable` listener. This closure will receive the event instance and the `Throwable` instance that caused the listener's failure: - -```php -use App\Events\PodcastProcessed; -use function Hypervel\Events\queueable; -use Hypervel\Support\Facades\Event; -use Throwable; - -Event::listen(queueable(function (PodcastProcessed $event) { - // ... -})->catch(function (PodcastProcessed $event, Throwable $e) { - // The queued listener failed... -})); -``` - -#### Wildcard Event Listeners - -You may even register listeners using the `*` as a wildcard parameter, allowing you to catch multiple events on the same listener. Wildcard listeners receive the event name as their first argument and the entire event data array as their second argument: - -```php -Event::listen('event.*', function (string $eventName, array $data) { - // ... -}); -``` - -## Defining Events - -An event class is essentially a data container which holds the information related to the event. For example, let's assume an `App\Events\OrderShipped` event receives an [Eloquent ORM](/docs/eloquent) object: - -```php -order... - } -} -``` - -::: note -Your event listeners may also type-hint any dependencies they need on their constructors. All event listeners are resolved via the Hypervel [service container](/docs/container), so dependencies will be injected automatically. -::: - -#### Stopping The Propagation Of An Event - -Sometimes, you may wish to stop the propagation of an event to other listeners. You may do so by returning `false` from your listener's `handle` method. - -## Queued Event Listeners - -Queueing listeners can be beneficial if your listener is going to perform a slow task such as sending an email or making an HTTP request. Before using queued listeners, make sure to [configure your queue](/docs/queues) and start a queue worker on your server or local development environment. - -To specify that a listener should be queued, add the `ShouldQueue` interface to the listener class. Listeners generated by the `make:listener` Artisan command already have this interface imported into the current namespace so you can use it immediately: - -```php -highPriority ? 0 : 60; -} -``` - -#### Conditionally Queueing Listeners - -Sometimes, you may need to determine whether a listener should be queued based on some data that are only available at runtime. To accomplish this, a `shouldQueue` method may be added to a listener to determine whether the listener should be queued. If the `shouldQueue` method returns `false`, the listener will not be executed: - -```php -order->subtotal >= 5000; - } -} -``` - -## Dispatching Events - -To dispatch an event, you may call the static `dispatch` method on the event. This method is made available on the event by the `Hypervel\Bus\Dispatchable` trait. Any arguments passed to the `dispatch` method will be passed to the event's constructor: - -```php -order_id); - - // Order shipment logic... - - OrderShipped::dispatch($order); - - return redirect('/orders'); - } -} -``` - - If you would like to conditionally dispatch an event, you may use the `dispatchIf` and `dispatchUnless` methods: - -```php -OrderShipped::dispatchIf($condition, $order); - -OrderShipped::dispatchUnless($condition, $order); -``` - -::: note -When testing, it can be helpful to assert that certain events were dispatched without actually triggering their listeners. Hypervel's [built-in testing helpers](#testing) make it a cinch. -::: - -### Dispatching Events After Database Transactions - -Sometimes, you may want to instruct Hypervel to only dispatch an event after the active database transaction has committed. To do so, you may implement the `ShouldDispatchAfterCommit` interface on the event class. - -This interface instructs Hypervel to not dispatch the event until the current database transaction is committed. If the transaction fails, the event will be discarded. If no database transaction is in progress when the event is dispatched, the event will be dispatched immediately: - -```php - 'Victoria Otwell']); - - $user->posts()->create(['title' => 'My first post!']); -}); -``` - -All events triggered within the closure will be dispatched after the closure is executed. This ensures that event listeners have access to all related records that were created during the deferred execution. If an exception occurs within the closure, the deferred events will not be dispatched. - -## Event Subscribers - -### Writing Event Subscribers - -Event subscribers are classes that may subscribe to multiple events from within the subscriber class itself, allowing you to define several event handlers within a single class. Subscribers should define a `subscribe` method, which will be passed an event dispatcher instance. You may call the `listen` method on the given dispatcher to register event listeners: - -```php -listen( - Login::class, - [UserEventSubscriber::class, 'handleUserLogin'] - ); - - $events->listen( - Logout::class, - [UserEventSubscriber::class, 'handleUserLogout'] - ); - } -} -``` - -If your event listener methods are defined within the subscriber itself, you may find it more convenient to return an array of events and method names from the subscriber's `subscribe` method. Hypervel will automatically determine the subscriber's class name when registering the event listeners: - -```php - - */ - public function subscribe(Dispatcher $events): array - { - return [ - Login::class => 'handleUserLogin', - Logout::class => 'handleUserLogout', - ]; - } -} -``` - -### Registering Event Subscribers - -After writing the subscriber, you are ready to register it with the event dispatcher. You may register subscribers using the `$subscribe` property on the `EventServiceProvider`. For example, let's add the `UserEventSubscriber` to the list: - -```php -order->id === $order->id; -}); -``` - -If you would simply like to assert that an event listener is listening to a given event, you may use the `assertListening` method: - -```php -Event::assertListening( - OrderShipped::class, - SendShipmentNotification::class -); -``` - -::: warning -After calling `Event::fake()`, no event listeners will be executed. So, if your tests use model factories that rely on events, such as creating a UUID during a model's `creating` event, you should call `Event::fake()` **after** using your factories. -::: - -### Faking a Subset of Events - -If you only want to fake event listeners for a specific set of events, you may pass them to the `fake` or `fakeFor` method: - -```php -/** - * Test order process. - */ -public function test_orders_can_be_processed(): void -{ - Event::fake([ - OrderCreated::class, - ]); - - $order = Order::factory()->create(); - - Event::assertDispatched(OrderCreated::class); - - // Other events are dispatched as normal... - $order->update([...]); -} -``` - -You may fake all events except for a set of specified events using the `except` method: - -```php -Event::fake()->except([ - OrderCreated::class, -]); -``` - -### Scoped Event Fakes - -If you only want to fake event listeners for a portion of your test, you may use the `fakeFor` method: - -```php -create(); - - Event::assertDispatched(OrderCreated::class); - - return $order; - }); - - // Events are dispatched as normal and observers will run ... - $order->update([...]); - } -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/facades.md b/_archive/docs/0.3/docs/facades.md deleted file mode 100644 index 0f30501de5..0000000000 --- a/_archive/docs/0.3/docs/facades.md +++ /dev/null @@ -1,214 +0,0 @@ -# Facades -[[toc]] - -## Introduction - -Throughout the Hypervel documentation, you will see examples of code that interacts with Hypervel's features via "facades". Facades provide a "static" interface to classes that are available in the application's [service container](/docs/container). Hypervel ships with many facades which provide access to almost all of Hypervel's features. - -Hypervel facades serve as "static proxies" to underlying classes in the service container, providing the benefit of a terse, expressive syntax while maintaining more testability and flexibility than traditional static methods. It's perfectly fine if you don't totally understand how facades work - just go with the flow and continue learning about Hypervel. - -All of Hypervel's facades are defined in the `Hypervel\Support\Facades` namespace. So, we can easily access a facade like so: - -```php -use Hypervel\Support\Facades\Cache; -use Hypervel\Support\Facades\Route; - -Route::get('/cache', function () { - return Cache::get('key'); -}); -``` - -Throughout the Hypervel documentation, many of the examples will use facades to demonstrate various features of the framework. - -#### Helper Functions - -To complement facades, Hypervel offers a variety of global "helper functions" that make it even easier to interact with common Laravel features. Some of the common helper functions you may interact with are `view`, `response`, `url`, `config`, and more. Each helper function offered by Laravel is documented with their corresponding feature; however, a complete list is available within the dedicated [helper documentation](/docs/helpers). - -For example, instead of using the `Hypervel\Support\Facades\Response` facade to generate a JSON response, we may simply use the `response` function. Because helper functions are globally available, you do not need to import any classes in order to use them: - -```php -use Hypervel\Support\Facades\Response; - -Route::get('/users', function () { - return Response::json([ - // ... - ]); -}); - -Route::get('/users', function () { - return response()->json([ - // ... - ]); -}); -``` - -## When to Utilize Facades - -Facades have many benefits. They provide a terse, memorable syntax that allows you to use Hypervel's features without remembering long class names that must be injected or configured manually. Furthermore, because of their unique usage of PHP's dynamic methods, they are easy to test. - -However, some care must be taken when using facades. The primary danger of facades is class "scope creep". Since facades are so easy to use and do not require injection, it can be easy to let your classes continue to grow and use many facades in a single class. Using dependency injection, this potential is mitigated by the visual feedback a large constructor gives you that your class is growing too large. So, when using facades, pay special attention to the size of your class so that its scope of responsibility stays narrow. If your class is getting too large, consider splitting it into multiple smaller classes. - -### Facades vs. Dependency Injection - -One of the primary benefits of dependency injection is the ability to swap implementations of the injected class. This is useful during testing since you can inject a mock or stub and assert that various methods were called on the stub. - -Typically, it would not be possible to mock or stub a truly static class method. However, since facades use dynamic methods to proxy method calls to objects resolved from the service container, we actually can test facades just as we would test an injected class instance. For example, given the following route: - -```php -use Hypervel\Support\Facades\Cache; - -Route::get('/cache', function () { - return Cache::get('key'); -}); -``` - -Using Hypervel 's facade testing methods, we can write the following test to verify that the `Cache::get` method was called with the argument we expected: - -```php -use Hypervel\Support\Facades\Cache; - -/** - * A basic functional test example. - */ -public function test_basic_example(): void -{ - Cache::shouldReceive('get') - ->with('key') - ->andReturn('value'); - - $response = $this->get('/cache'); - - $response->assertSee('value'); -} -``` - -### Facades vs. Helper Functions - -In addition to facades, Hypervel includes a variety of "helper" functions which can perform common tasks like generating views, firing events, or sending HTTP responses. Many of these helper functions perform the same function as a corresponding facade. For example, this facade call and helper call are equivalent: - -```php -return Hypervel\Support\Facades\View::render('profile'); - -return view('profile'); -``` - -There is absolutely no practical difference between facades and helper functions. When using helper functions, you may still test them exactly as you would the corresponding facade. For example, given the following route: - -```php -Route::get('/cache', function () { - return cache('key'); -}); -``` - -The `cache` helper is going to call the `get` method on the class underlying the `Cache` facade. So, even though we are using the helper function, we can write the following test to verify that the method was called with the argument we expected: - -```php -use Hypervel\Support\Facades\Cache; - -/** - * A basic functional test example. - */ -public function test_basic_example(): void -{ - Cache::shouldReceive('get') - ->with('key') - ->andReturn('value'); - - $response = $this->get('/cache'); - - $response->assertSee('value'); -} -``` - -## How Facades Work - -In a Laravel application, a facade is a class that provides access to an object from the container. The machinery that makes this work is in the `Facade` class. Hypervel's facades, and any custom facades you create, will extend the base `Hypervel\Support\Facades\Facade` class. - -The `Facade` base class makes use of the `__callStatic()` magic-method to defer calls from your facade to an object resolved from the container. In the example below, a call is made to the Hypervel cache system. By glancing at this code, one might assume that the static `get` method is being called on the `Cache` class: - -```php - $user]); - } -} -``` - -Notice that near the top of the file we are "importing" the `Cache` facade. This facade serves as a proxy for accessing the underlying implementation of the `Hypervel\Cache\Contracts\Factory` interface. Any calls we make using the facade will be passed to the underlying instance of Hypervel's cache service. - -If we look at that `Hypervel\Support\Facades\Cache` class, you'll see that there is no static method `get`: - -```php -use Hypervel\Cache\Contracts\Factory; - -class Cache extends Facade -{ - /** - * Get the registered name of the component. - */ - protected static function getFacadeAccessor(): string - { - return Factory::class; - } -} -``` - -Instead, the `Cache` facade extends the base `Facade` class and defines the method `getFacadeAccessor()`. This method's job is to return the name of a service container binding. When a user references any static method on the `Cache` facade, Hypervel resolves the `cache` binding from the [service container](/docs/container) and runs the requested method (in this case, `get`) against that object. - -## Facade Class Reference - -Below you will find every facade and its underlying class. This is a useful tool for quickly digging into the API documentation for a given facade root. The [service container binding](/docs/container) key is also included where applicable. - -Facade | Class | Service Container Binding -------------- | ------------- | ------------- -App | Hypervel\Foundation\Application | `app` -Artisan | Hypervel\Foundation\Console\Contracts\Kernel | `artisan` -Auth | Hypervel\Auth\AuthManager | `auth` -Auth (Instance) | Hypervel\Auth\Contracts\Guard | `auth.driver` -Blade | Hyperf\ViewEngine\Compiler\CompilerInterface | `blade.compiler` -Broadcast | Hypervel\Broadcasting\Contracts\Factory |   -Bus | Hypervel\Bus\Contracts\Dispatcher |   -Cache | Hypervel\Cache\CacheManager | `cache` -Cache (Instance) | Hypervel\Cache\Repository | `cache.store` -Config | Hypervel\Config\Contracts\Repository | `config` -Cookie | Hypervel\Cookie\CookieManager | `cookie` -Crypt | Hypervel\Encryption\Encrypter | `encrypter` -DB | Hyperf\DbConnection\Db | `db` -Event | Psr\EventDispatcher\EventDispatcherInterface | `events` -Environment | Hypervel\Support\Environment |   -File | Hypervel\Filesystem\Filesystem | `files` -Gate | Hypervel\Auth\Contracts\Gate |   -Hash | Hypervel\Hashing\Contracts\Hasher | `hash` -Http | Hypervel\HttpClient\Factory |   -Lang | Hypervel\Translation\Contracts\Translator | `translator` -Log | Psr\Log\LoggerInterface | `log` -Mail | Hypervel\Mail\Contracts\Factory | `mailer` -Notification | Hypervel\Notifications\Contracts\Dispatcher |   -Queue | Hypervel\Queue\Contracts\Factory | `queue` -RateLimiter | Hypervel\Cache\RateLimiter |   -Redis | Hyperf\Redis\Redis | `redis` -Request | Hypervel\Http\Contracts\RequestContract | `request` -Response | Hypervel\Http\Contracts\ResponseContract | `response` -Route | Hypervel\Router\Router | `router` -Schedule | Hypervel\Foundation\Console\Contracts\Schedule | `schedule` -Schema | Hypervel\Database\Schema\SchemaProxy | `db.schema` -Session | Hypervel\Session\Contracts\Session | `session` -Storage | Hypervel\Filesystem\Contracts\Factory | `filesystem` -URL | Hypervel\Router\UrlGenerator | `url` -Validator | Hypervel\Validation\Contracts\Factory | `validator` -View | Hyperf\View\RenderInterface | `view` diff --git a/_archive/docs/0.3/docs/filesystem.md b/_archive/docs/0.3/docs/filesystem.md deleted file mode 100644 index d83806d409..0000000000 --- a/_archive/docs/0.3/docs/filesystem.md +++ /dev/null @@ -1,778 +0,0 @@ -## Introduction - -Hypervel provides a powerful filesystem abstraction thanks to the wonderful [Flysystem](https://github.com/thephpleague/flysystem) PHP package by Frank de Jonge. The Hypervel Flysystem integration provides simple drivers for working with local filesystems, SFTP, Amazon S3 and Google Cloud Storage. Even better, it's amazingly simple to switch between these storage options between your local development machine and production server as the API remains the same for each system. - -## Configuration - -Hypervel's filesystem configuration file is located at `config/filesystems.php`. Within this file, you may configure all of your filesystem "disks". Each disk represents a particular storage driver and storage location. Example configurations for each supported driver are included in the configuration file so you can modify the configuration to reflect your storage preferences and credentials. - -The `local` driver interacts with files stored locally on the server running the Hypervel application while the `s3` driver is used to write to Amazon's S3 cloud storage service. - -::: note -You may configure as many disks as you like and may even have multiple disks that use the same driver. -::: - -### The Local Driver - -When using the `local` driver, all file operations are relative to the `root` directory defined in your `filesystems` configuration file. By default, this value is set to the `storage/app` directory. Therefore, the following method would write to `storage/app/example.txt`: - -```php -use Hypervel\Support\Facades\Storage; - -Storage::disk('local')->put('example.txt', 'Contents'); -``` - -### Driver Pool - -Hypervel provides an elegant way to create object pools for your disk drivers. Object pools can keep your drivers safe in coroutines and help you control the concurrency number for each disk. - -`s3` and `gcs` drivers are configured as object pools by default. You can customize the pool config for your disks in the `filesystems.php` config: - -```php -'s3' => [ - 'driver' => 's3', - // ... - 'pool' => [ - 'min_objects' => 1, - 'max_objects' => 10, - 'wait_timeout' => 3.0, - 'max_lifetime' => 60.0, - ], -], -``` - -You will get a `RuntimeException` if your driver instances exceeds the `max_objects` in the config while handling concurrent requests. You should configure these values according to your needs. - -::: note -Pool config is not supported in `ftp`, `sftp` and `local` drivers. They are coroutine-safe with singletons. -::: - -### Driver Prerequisites - -#### S3 Driver Configuration - -Before using the S3 driver, you will need to install the Flysystem S3 package via the Composer package manager: - -```shell -composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies -``` - -The S3 driver configuration information is located in your `config/filesystems.php` configuration file. This file contains an example configuration array for an S3 driver. You are free to modify this array with your own S3 configuration and credentials. For convenience, these environment variables match the naming convention used by the AWS CLI. - -#### FTP Driver Configuration - -Before using the FTP driver, you will need to install the Flysystem FTP package via the Composer package manager: - -```shell -composer require league/flysystem-ftp "^3.0" -``` - -Hypervel 's Flysystem integrations work great with FTP; however, a sample configuration is not included with the framework's default `filesystems.php` configuration file. If you need to configure an FTP filesystem, you may use the configuration example below: - -```php -'ftp' => [ - 'driver' => 'ftp', - 'host' => env('FTP_HOST'), - 'username' => env('FTP_USERNAME'), - 'password' => env('FTP_PASSWORD'), - - // Optional FTP Settings... - // 'port' => env('FTP_PORT', 21), - // 'root' => env('FTP_ROOT'), - // 'passive' => true, - // 'ssl' => true, - // 'timeout' => 30, -], -``` - -#### SFTP Driver Configuration - -Before using the SFTP driver, you will need to install the Flysystem SFTP package via the Composer package manager: - -```shell -composer require league/flysystem-sftp-v3 "^3.0" -``` - -Hypervel's Flysystem integrations work great with SFTP; however, a sample configuration is not included with the framework's default `filesystems.php` configuration file. If you need to configure an SFTP filesystem, you may use the configuration example below: - -```php -'sftp' => [ - 'driver' => 'sftp', - 'host' => env('SFTP_HOST'), - - // Settings for basic authentication... - 'username' => env('SFTP_USERNAME'), - 'password' => env('SFTP_PASSWORD'), - - // Settings for SSH key based authentication with encryption password... - 'privateKey' => env('SFTP_PRIVATE_KEY'), - 'passphrase' => env('SFTP_PASSPHRASE'), - - // Settings for file / directory permissions... - 'visibility' => 'private', // `private` = 0600, `public` = 0644 - 'directory_visibility' => 'private', // `private` = 0700, `public` = 0755 - - // Optional SFTP Settings... - // 'hostFingerprint' => env('SFTP_HOST_FINGERPRINT'), - // 'maxTries' => 4, - // 'passphrase' => env('SFTP_PASSPHRASE'), - // 'port' => env('SFTP_PORT', 22), - // 'root' => env('SFTP_ROOT', ''), - // 'timeout' => 30, - // 'useAgent' => true, -], -``` - -### Scoped and Read-Only Filesystems - -Scoped disks allow you to define a filesystem where all paths are automatically prefixed with a given path prefix. Before creating a scoped filesystem disk, you will need to install an additional Flysystem package via the Composer package manager: - -```shell -composer require league/flysystem-path-prefixing "^3.0" -``` - -You may create a path scoped instance of any existing filesystem disk by defining a disk that utilizes the `scoped` driver. For example, you may create a disk which scopes your existing `s3` disk to a specific path prefix, and then every file operation using your scoped disk will utilize the specified prefix: - -```php -'s3-videos' => [ - 'driver' => 'scoped', - 'disk' => 's3', - 'prefix' => 'path/to/videos', -], -``` - -"Read-only" disks allow you to create filesystem disks that do not allow write operations. Before using the `read-only` configuration option, you will need to install an additional Flysystem package via the Composer package manager: - -```shell -composer require league/flysystem-read-only "^3.0" -``` - -Next, you may include the `read-only` configuration option in one or more of your disk's configuration arrays: - -```php -'s3-videos' => [ - 'driver' => 's3', - // ... - 'read-only' => true, -], -``` - -### Amazon S3 Compatible Filesystems - -By default, your application's `filesystems` configuration file contains a disk configuration for the `s3` disk. In addition to using this disk to interact with Amazon S3, you may use it to interact with any S3 compatible file storage service such as [MinIO](https://github.com/minio/minio) or [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces/). - -Typically, after updating the disk's credentials to match the credentials of the service you are planning to use, you only need to update the value of the `endpoint` configuration option. This option's value is typically defined via the `AWS_ENDPOINT` environment variable: - -```php -'endpoint' => env('AWS_ENDPOINT', 'https://minio:9000'), -``` - -#### MinIO - -In order for Hypervel's Flysystem integration to generate proper URLs when using MinIO, you should define the `AWS_URL` environment variable so that it matches your application's local URL and includes the bucket name in the URL path: - -```ini -AWS_URL=http://localhost:9000/local -``` - -::: warning -Generating temporary storage URLs via the `temporaryUrl` method is not supported when using MinIO. -::: - -## Obtaining Disk Instances - -The `Storage` facade may be used to interact with any of your configured disks. For example, you may use the `put` method on the facade to store an avatar on the default disk. If you call methods on the `Storage` facade without first calling the `disk` method, the method will automatically be passed to the default disk: - -```php -use Hypervel\Support\Facades\Storage; - -Storage::put('avatars/1', $content); -``` - -If your application interacts with multiple disks, you may use the `disk` method on the `Storage` facade to work with files on a particular disk: - -```php -Storage::disk('s3')->put('avatars/1', $content); -``` - -### On-Demand Disks - -Sometimes you may wish to create a disk at runtime using a given configuration without that configuration actually being present in your application's `filesystems` configuration file. To accomplish this, you may pass a configuration array to the `Storage` facade's `build` method: - -```php -use Hypervel\Support\Facades\Storage; - -$disk = Storage::build([ - 'driver' => 'local', - 'root' => '/path/to/root', -]); - -$disk->put('image.jpg', $content); -``` - -## Retrieving Files - -The `get` method may be used to retrieve the contents of a file. The raw string contents of the file will be returned by the method. Remember, all file paths should be specified relative to the disk's "root" location: - -```php -$contents = Storage::get('file.jpg'); -``` - -If the file you are retrieving contains JSON, you may use the `json` method to retrieve the file and decode its contents: - -```php -$orders = Storage::json('orders.json'); -``` - -The `exists` method may be used to determine if a file exists on the disk: - -```php -if (Storage::disk('s3')->exists('file.jpg')) { - // ... -} -``` - -The `missing` method may be used to determine if a file is missing from the disk: - -```php -if (Storage::disk('s3')->missing('file.jpg')) { - // ... -} -``` - -### Downloading Files - -The `download` method may be used to generate a response that forces the user's browser to download the file at the given path. The `download` method accepts a filename as the second argument to the method, which will determine the filename that is seen by the user downloading the file. Finally, you may pass an array of HTTP headers as the third argument to the method: - -```php -return Storage::download('file.jpg'); - -return Storage::download('file.jpg', $name, $headers); -``` - -### File URLs - -You may use the `url` method to get the URL for a given file. If you are using the `local` driver, this will typically just prepend `/storage` to the given path and return a relative URL to the file. If you are using the `s3` driver, the fully qualified remote URL will be returned: - -```php -use Hypervel\Support\Facades\Storage; - -$url = Storage::url('file.jpg'); -``` - -When using the `local` driver, all files that should be publicly accessible should be placed in the `storage/app/public` directory. Furthermore, you should create a symbolic link at `public/storage` which points to the `storage/app/public` directory. - -::: warning -When using the `local` driver, the return value of `url` is not URL encoded. For this reason, we recommend always storing your files using names that will create valid URLs. -::: - -#### URL Host Customization - -If you would like to pre-define the host for URLs generated using the `Storage` facade, you may add a `url` option to the disk's configuration array: - -```php -'public' => [ - 'driver' => 'local', - 'root' => storage_path('app/public'), - 'url' => env('APP_URL').'/storage', - 'visibility' => 'public', -], -``` - -### Temporary URLs - -Using the `temporaryUrl` method, you may create temporary URLs to files stored using the `s3` driver. This method accepts a path and a `DateTime` instance specifying when the URL should expire: - -```php -use Hypervel\Support\Facades\Storage; - -$url = Storage::temporaryUrl( - 'file.jpg', now()->addMinutes(5) -); -``` - -If you need to specify additional [S3 request parameters](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html#RESTObjectGET-requests), you may pass the array of request parameters as the third argument to the `temporaryUrl` method: - -```php -$url = Storage::temporaryUrl( - 'file.jpg', - now()->addMinutes(5), - [ - 'ResponseContentType' => 'application/octet-stream', - 'ResponseContentDisposition' => 'attachment; filename=file2.jpg', - ] -); -``` - -If you need to customize how temporary URLs are created for a specific storage disk, you can use the `buildTemporaryUrlsUsing` method. For example, this can be useful if you have a controller that allows you to download files stored via a disk that doesn't typically support temporary URLs. Usually, this method should be called from the `boot` method of a service provider: - -```php -buildTemporaryUrlsUsing( - function (string $path, DateTime $expiration, array $options) { - return URL::temporarySignedRoute( - 'files.download', - $expiration, - array_merge($options, ['path' => $path]) - ); - } - ); - } -} -``` - -#### Temporary Upload URLs - -::: warning -The ability to generate temporary upload URLs is only supported by the `s3` driver. -::: - -If you need to generate a temporary URL that can be used to upload a file directly from your client-side application, you may use the `temporaryUploadUrl` method. This method accepts a path and a `DateTime` instance specifying when the URL should expire. The `temporaryUploadUrl` method returns an associative array which may be destructured into the upload URL and the headers that should be included with the upload request: - -```php -use Hypervel\Support\Facades\Storage; - -['url' => $url, 'headers' => $headers] = Storage::temporaryUploadUrl( - 'file.jpg', now()->addMinutes(5) -); -``` - -This method is primarily useful in serverless environments that require the client-side application to directly upload files to a cloud storage system such as Amazon S3. - -### File Metadata - -In addition to reading and writing files, Hypervel can also provide information about the files themselves. For example, the `size` method may be used to get the size of a file in bytes: - -```php -use Hypervel\Support\Facades\Storage; - -$size = Storage::size('file.jpg'); -``` - -The `lastModified` method returns the UNIX timestamp of the last time the file was modified: - -```php -$time = Storage::lastModified('file.jpg'); -``` - -The MIME type of a given file may be obtained via the `mimeType` method: - -```php -$mime = Storage::mimeType('file.jpg'); -``` - -#### File Paths - -You may use the `path` method to get the path for a given file. If you are using the `local` driver, this will return the absolute path to the file. If you are using the `s3` driver, this method will return the relative path to the file in the S3 bucket: - -```php -use Hypervel\Support\Facades\Storage; - -$path = Storage::path('file.jpg'); -``` - -## Storing Files - -The `put` method may be used to store file contents on a disk. You may also pass a PHP `resource` to the `put` method, which will use Flysystem's underlying stream support. Remember, all file paths should be specified relative to the "root" location configured for the disk: - -```php -use Hypervel\Support\Facades\Storage; - -Storage::put('file.jpg', $contents); - -Storage::put('file.jpg', $resource); -``` - -#### Failed Writes - -If the `put` method (or other "write" operations) is unable to write the file to disk, `false` will be returned: - -```php -if (! Storage::put('file.jpg', $contents)) { - // The file could not be written to disk... -} -``` - -If you wish, you may define the `throw` option within your filesystem disk's configuration array. When this option is defined as `true`, "write" methods such as `put` will throw an instance of `League\Flysystem\UnableToWriteFile` when write operations fail: - -```php -'public' => [ - 'driver' => 'local', - // ... - 'throw' => true, -], -``` - -### Prepending and Appending To Files - -The `prepend` and `append` methods allow you to write to the beginning or end of a file: - -```php -Storage::prepend('file.log', 'Prepended Text'); - -Storage::append('file.log', 'Appended Text'); -``` - -### Copying and Moving Files - -The `copy` method may be used to copy an existing file to a new location on the disk, while the `move` method may be used to rename or move an existing file to a new location: - -```php -Storage::copy('old/file.jpg', 'new/file.jpg'); - -Storage::move('old/file.jpg', 'new/file.jpg'); -``` - -### Automatic Streaming - -Streaming files to storage offers significantly reduced memory usage. If you would like Hypervel to automatically manage streaming a given file to your storage location, you may use the `putFile` or `putFileAs` method. This method accepts either a path string or `Hypervel\Http\UploadedFile` instance and will automatically stream the file to your desired location: - -```php -use Hypervel\Support\Facades\Storage; - -// Automatically generate a unique ID for filename... -$path = Storage::putFile('photos', '/path/to/photo')); - -// Manually specify a filename... -$path = Storage::putFileAs('photos', '/path/to/photo', 'photo.jpg'); -``` - -There are a few important things to note about the `putFile` method. Note that we only specified a directory name and not a filename. By default, the `putFile` method will generate a unique ID to serve as the filename. The file's extension will be determined by examining the file's MIME type. The path to the file will be returned by the `putFile` method so you can store the path, including the generated filename, in your database. - -The `putFile` and `putFileAs` methods also accept an argument to specify the "visibility" of the stored file. This is particularly useful if you are storing the file on a cloud disk such as Amazon S3 and would like the file to be publicly accessible via generated URLs: - -```php -Storage::putFile('photos', '/path/to/photo', 'public'); -``` - -### File Uploads - -In web applications, one of the most common use-cases for storing files is storing user uploaded files such as photos and documents. Hypervel makes it very easy to store uploaded files using the `store` method on an uploaded file instance. Call the `store` method with the path at which you wish to store the uploaded file: - -```php -file('avatar')->store('avatars'); - - return $path; - } -} -``` - -There are a few important things to note about this example. Note that we only specified a directory name, not a filename. By default, the `store` method will generate a unique ID to serve as the filename. The file's extension will be determined by examining the file's MIME type. The path to the file will be returned by the `store` method so you can store the path, including the generated filename, in your database. - -You may also call the `putFile` method on the `Storage` facade to perform the same file storage operation as the example above: - -```php -$path = Storage::putFile('avatars', $request->file('avatar')); -``` - -#### Specifying a File Name - -If you do not want a filename to be automatically assigned to your stored file, you may use the `storeAs` method, which receives the path, the filename, and the (optional) disk as its arguments: - -```php -$path = $request->file('avatar')->storeAs( - 'avatars', $request->user()->id -); -``` - -You may also use the `putFileAs` method on the `Storage` facade, which will perform the same file storage operation as the example above: - -```php -$path = Storage::putFileAs( - 'avatars', $request->file('avatar'), $request->user()->id -); -``` - -::: warning -Unprintable and invalid unicode characters will automatically be removed from file paths. Therefore, you may wish to sanitize your file paths before passing them to Hypervel's file storage methods. File paths are normalized using the `League\Flysystem\WhitespacePathNormalizer::normalizePath` method. -::: - - -#### Specifying a Disk - -By default, this uploaded file's `store` method will use your default disk. If you would like to specify another disk, pass the disk name as the second argument to the `store` method: - -```php -$path = $request->file('avatar')->store( - 'avatars/'.$request->user()->id, 's3' -); -``` - -If you are using the `storeAs` method, you may pass the disk name as the third argument to the method: - -```php -$path = $request->file('avatar')->storeAs( - 'avatars', - $request->user()->id, - 's3' -); -``` - -#### Other Uploaded File Information - -If you would like to get the original name and extension of the uploaded file, you may do so using the `getClientOriginalName` and `getClientOriginalExtension` methods: - -```php -$file = $request->file('avatar'); - -$name = $file->getClientOriginalName(); -$extension = $file->getClientOriginalExtension(); -``` - -However, keep in mind that the `getClientOriginalName` and `getClientOriginalExtension` methods are considered unsafe, as the file name and extension may be tampered with by a malicious user. For this reason, you should typically prefer the `hashName` and `extension` methods to get a name and an extension for the given file upload: - -```php -$file = $request->file('avatar'); - -$name = $file->hashName(); // Generate a unique, random name... -$extension = $file->extension(); // Determine the file's extension based on the file's MIME type... -``` - -### File Visibility - -In Hypervel's Flysystem integration, "visibility" is an abstraction of file permissions across multiple platforms. Files may either be declared `public` or `private`. When a file is declared `public`, you are indicating that the file should generally be accessible to others. For example, when using the S3 driver, you may retrieve URLs for `public` files. - -You can set the visibility when writing the file via the `put` method: - -```php -use Hypervel\Support\Facades\Storage; - -Storage::put('file.jpg', $contents, 'public'); -``` - -If the file has already been stored, its visibility can be retrieved and set via the `getVisibility` and `setVisibility` methods: - -```php -$visibility = Storage::getVisibility('file.jpg'); - -Storage::setVisibility('file.jpg', 'public'); -``` - -When interacting with uploaded files, you may use the `storePublicly` and `storePubliclyAs` methods to store the uploaded file with `public` visibility: - -```php -$path = $request->file('avatar')->storePublicly('avatars', 's3'); - -$path = $request->file('avatar')->storePubliclyAs( - 'avatars', - $request->user()->id, - 's3' -); -``` - -#### Local Files and Visibility - -When using the `local` driver, `public` [visibility](#file-visibility) translates to `0755` permissions for directories and `0644` permissions for files. You can modify the permissions mappings in your application's `filesystems` configuration file: - -```php -'local' => [ - 'driver' => 'local', - 'root' => storage_path('app'), - 'permissions' => [ - 'file' => [ - 'public' => 0644, - 'private' => 0600, - ], - 'dir' => [ - 'public' => 0755, - 'private' => 0700, - ], - ], -], -``` - -## Deleting Files - -The `delete` method accepts a single filename or an array of files to delete: - -```php -use Hypervel\Support\Facades\Storage; - -Storage::delete('file.jpg'); - -Storage::delete(['file.jpg', 'file2.jpg']); -``` - -If necessary, you may specify the disk that the file should be deleted from: - -```php -use Hypervel\Support\Facades\Storage; - -Storage::disk('s3')->delete('path/file.jpg'); -``` - -## Directories - -#### Get All Files Within a Directory - -The `files` method returns an array of all of the files in a given directory. If you would like to retrieve a list of all files within a given directory including all subdirectories, you may use the `allFiles` method: - -```php -use Hypervel\Support\Facades\Storage; - -$files = Storage::files($directory); - -$files = Storage::allFiles($directory); -``` - -#### Get All Directories Within a Directory - -The `directories` method returns an array of all the directories within a given directory. Additionally, you may use the `allDirectories` method to get a list of all directories within a given directory and all of its subdirectories: - -```php -$directories = Storage::directories($directory); - -$directories = Storage::allDirectories($directory); -``` - -#### Create a Directory - -The `makeDirectory` method will create the given directory, including any needed subdirectories: - -```php -Storage::makeDirectory($directory); -``` - -#### Delete a Directory - -Finally, the `deleteDirectory` method may be used to remove a directory and all of its files: - -```php -Storage::deleteDirectory($directory); -``` - -## Testing - -The `Storage` facade's `fake` method allows you to easily generate a fake disk that, combined with the file generation utilities of the `Hypervel\Http\UploadedFile` class, greatly simplifies the testing of file uploads. For example: - -```php -json('POST', '/photos', [ - UploadedFile::fake()->image('photo1.jpg'), - UploadedFile::fake()->image('photo2.jpg') - ]); - - // Assert one or more files were stored... - Storage::disk('photos')->assertExists('photo1.jpg'); - Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']); - - // Assert one or more files were not stored... - Storage::disk('photos')->assertMissing('missing.jpg'); - Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']); - - // Assert that a given directory is empty... - Storage::disk('photos')->assertDirectoryEmpty('/wallpapers'); - } -} -``` - -By default, the `fake` method will delete all files in its temporary directory. If you would like to keep these files, you may use the "persistentFake" method instead. For more information on testing file uploads, you may consult the [HTTP testing documentation's information on file uploads](/docs/http-tests#testing-file-uploads). - -::: warning -The `image` method requires the [GD extension](https://www.php.net/manual/en/book.image.php). -::: - -## Custom Filesystems - -Hypervel's Flysystem integration provides support for several "drivers" out of the box; however, Flysystem is not limited to these and has adapters for many other storage systems. You can create a custom driver if you want to use one of these additional adapters in your Hypervel application. - -In order to define a custom filesystem you will need a Flysystem adapter. Let's add a community maintained Dropbox adapter to our project: - -```shell -composer require spatie/flysystem-dropbox -``` - -Next, you can register the driver within the `boot` method of one of your application's [service providers](/docs/providers). To accomplish this, you should use the `extend` method of the `Storage` facade: - -```php -user()->fill([ - 'password' => Hash::make($request->newPassword) - ])->save(); - - return redirect('/profile'); - } -} -``` - -#### Adjusting The Bcrypt Work Factor - -If you are using the Bcrypt algorithm, the `make` method allows you to manage the work factor of the algorithm using the `rounds` option; however, the default work factor managed by Hypervel is acceptable for most applications: - -```php -$hashed = Hash::make('password', [ - 'rounds' => 12, -]); -``` - -#### Adjusting The Argon2 Work Factor - -If you are using the Argon2 algorithm, the `make` method allows you to manage the work factor of the algorithm using the `memory`, `time`, and `threads` options; however, the default values managed by Hypervel are acceptable for most applications: - -```php -$hashed = Hash::make('password', [ - 'memory' => 1024, - 'time' => 2, - 'threads' => 2, -]); -``` - -::: note -For more information on these options, please refer to the [official PHP documentation regarding Argon hashing](https://secure.php.net/manual/en/function.password-hash.php). -::: - -### Verifying That a Password Matches a Hash - -The `check` method provided by the `Hash` facade allows you to verify that a given plain-text string corresponds to a given hash: - -```php -if (Hash::check('plain-text', $hashedPassword)) { - // The passwords match... -} -``` - -### Determining if a Password Needs to be Rehashed - -The `needsRehash` method provided by the `Hash` facade allows you to determine if the work factor used by the hasher has changed since the password was hashed. Some applications choose to perform this check during the application's authentication process: - -```php -if (Hash::needsRehash($hashed)) { - $hashed = Hash::make('plain-text'); -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/helpers.md b/_archive/docs/0.3/docs/helpers.md deleted file mode 100644 index 94188b6b89..0000000000 --- a/_archive/docs/0.3/docs/helpers.md +++ /dev/null @@ -1,2568 +0,0 @@ -## Introduction - -Hypervel includes a variety of global "helper" PHP functions. Many of these functions are used by the framework itself; however, you are free to use them in your own applications if you find them convenient. - -## Available Methods - - - - -### Arrays & Objects - -
    - -[Arr::accessible](#method-array-accessible) -[Arr::add](#method-array-add) -[Arr::collapse](#method-array-collapse) -[Arr::crossJoin](#method-array-crossjoin) -[Arr::divide](#method-array-divide) -[Arr::dot](#method-array-dot) -[Arr::except](#method-array-except) -[Arr::exists](#method-array-exists) -[Arr::first](#method-array-first) -[Arr::flatten](#method-array-flatten) -[Arr::forget](#method-array-forget) -[Arr::get](#method-array-get) -[Arr::has](#method-array-has) -[Arr::hasAny](#method-array-hasany) -[Arr::isAssoc](#method-array-isassoc) -[Arr::isList](#method-array-islist) -[Arr::join](#method-array-join) -[Arr::keyBy](#method-array-keyby) -[Arr::last](#method-array-last) -[Arr::map](#method-array-map) -[Arr::mapWithKeys](#method-array-map-with-keys) -[Arr::only](#method-array-only) -[Arr::pluck](#method-array-pluck) -[Arr::prepend](#method-array-prepend) -[Arr::prependKeysWith](#method-array-prependkeyswith) -[Arr::pull](#method-array-pull) -[Arr::query](#method-array-query) -[Arr::random](#method-array-random) -[Arr::set](#method-array-set) -[Arr::shuffle](#method-array-shuffle) -[Arr::sort](#method-array-sort) -[Arr::sortDesc](#method-array-sort-desc) -[Arr::sortRecursive](#method-array-sort-recursive) -[Arr::sortRecursiveDesc](#method-array-sort-recursive-desc) -[Arr::toCssClasses](#method-array-to-css-classes) -[Arr::toCssStyles](#method-array-to-css-styles) -[Arr::undot](#method-array-undot) -[Arr::where](#method-array-where) -[Arr::whereNotNull](#method-array-where-not-null) -[Arr::wrap](#method-array-wrap) -[data_fill](#method-data-fill) -[data_get](#method-data-get) -[data_set](#method-data-set) -[data_forget](#method-data-forget) -[head](#method-head) -[last](#method-last) -
    - - -### Numbers - -
    - -[Number::abbreviate](#method-number-abbreviate) -[Number::clamp](#method-number-clamp) -[Number::currency](#method-number-currency) -[Number::fileSize](#method-number-file-size) -[Number::forHumans](#method-number-for-humans) -[Number::format](#method-number-format) -[Number::ordinal](#method-number-ordinal) -[Number::percentage](#method-number-percentage) -[Number::spell](#method-number-spell) -[Number::useLocale](#method-number-use-locale) -[Number::withLocale](#method-number-with-locale) - -
    - - - -### Paths - -
    - -[app_path](#method-app-path) -[base_path](#method-base-path) -[config_path](#method-config-path) -[database_path](#method-database-path) -[lang_path](#method-lang-path) -[public_path](#method-public-path) -[resource_path](#method-resource-path) -[storage_path](#method-storage-path) - -
    - - -### URLs - -
    - -[asset](#method-asset) -[route](#method-route) -[secure_asset](#method-secure-asset) -[secure_url](#method-secure-url) -[to_route](#method-to-route) -[url](#method-url) - -
    - - -### Miscellaneous - -
    - -[abort](#method-abort) -[abort_if](#method-abort-if) -[abort_unless](#method-abort-unless) -[app](#method-app) -[auth](#method-auth) -[bcrypt](#method-bcrypt) -[blank](#method-blank) -[cache](#method-cache) -[class_uses_recursive](#method-class-uses-recursive) -[collect](#method-collect) -[config](#method-config) -[cookie](#method-cookie) -[decrypt](#method-decrypt) -[dd](#method-dd) -[dispatch](#method-dispatch) -[dump](#method-dump) -[encrypt](#method-encrypt) -[env](#method-env) -[event](#method-event) -[filled](#method-filled) -[info](#method-info) -[logger](#method-logger) -[method_field](#method-method-field) -[now](#method-now) -[optional](#method-optional) -[policy](#method-policy) -[redirect](#method-redirect) -[report](#method-report) -[report_if](#method-report-if) -[report_unless](#method-report-unless) -[request](#method-request) -[rescue](#method-rescue) -[resolve](#method-resolve) -[response](#method-response) -[retry](#method-retry) -[session](#method-session) -[tap](#method-tap) -[throw_if](#method-throw-if) -[throw_unless](#method-throw-unless) -[today](#method-today) -[trait_uses_recursive](#method-trait-uses-recursive) -[transform](#method-transform) -[validator](#method-validator) -[value](#method-value) -[view](#method-view) -[with](#method-with) - -
    - - -## Arrays & Objects - - -#### `Arr::accessible()` {.collection-method .first-collection-method} - -The `Arr::accessible` method determines if the given value is array accessible: - -```php -use Hypervel\Support\Arr; -use Hyperf\Collection\Collection; - -$isAccessible = Arr::accessible(['a' => 1, 'b' => 2]); - -// true - -$isAccessible = Arr::accessible(new Collection); - -// true - -$isAccessible = Arr::accessible('abc'); - -// false - -$isAccessible = Arr::accessible(new stdClass); - -// false -``` - - -#### `Arr::add()` - -The `Arr::add` method adds a given key / value pair to an array if the given key doesn't already exist in the array or is set to `null`: - -```php -use Hypervel\Support\Arr; - -$array = Arr::add(['name' => 'Desk'], 'price', 100); - -// ['name' => 'Desk', 'price' => 100] - -$array = Arr::add(['name' => 'Desk', 'price' => null], 'price', 100); - -// ['name' => 'Desk', 'price' => 100] -``` - - -#### `Arr::collapse()` - -The `Arr::collapse` method collapses an array of arrays into a single array: - -```php -use Hypervel\Support\Arr; - -$array = Arr::collapse([[1, 2, 3], [4, 5, 6], [7, 8, 9]]); - -// [1, 2, 3, 4, 5, 6, 7, 8, 9] -``` - - -#### `Arr::crossJoin()` - -The `Arr::crossJoin` method cross joins the given arrays, returning a Cartesian product with all possible permutations: - -```php -use Hypervel\Support\Arr; - -$matrix = Arr::crossJoin([1, 2], ['a', 'b']); - -/* - [ - [1, 'a'], - [1, 'b'], - [2, 'a'], - [2, 'b'], - ] -*/ - -$matrix = Arr::crossJoin([1, 2], ['a', 'b'], ['I', 'II']); - -/* - [ - [1, 'a', 'I'], - [1, 'a', 'II'], - [1, 'b', 'I'], - [1, 'b', 'II'], - [2, 'a', 'I'], - [2, 'a', 'II'], - [2, 'b', 'I'], - [2, 'b', 'II'], - ] -*/ -``` - - -#### `Arr::divide()` - -The `Arr::divide` method returns two arrays: one containing the keys and the other containing the values of the given array: - -```php -use Hypervel\Support\Arr; - -[$keys, $values] = Arr::divide(['name' => 'Desk']); - -// $keys: ['name'] - -// $values: ['Desk'] -``` - - -#### `Arr::dot()` - -The `Arr::dot` method flattens a multi-dimensional array into a single level array that uses "dot" notation to indicate depth: - -```php -use Hypervel\Support\Arr; - -$array = ['products' => ['desk' => ['price' => 100]]]; - -$flattened = Arr::dot($array); - -// ['products.desk.price' => 100] -``` - - -#### `Arr::except()` - -The `Arr::except` method removes the given key / value pairs from an array: - -```php -use Hypervel\Support\Arr; - -$array = ['name' => 'Desk', 'price' => 100]; - -$filtered = Arr::except($array, ['price']); - -// ['name' => 'Desk'] -``` - - -#### `Arr::exists()` - -The `Arr::exists` method checks that the given key exists in the provided array: - -```php -use Hypervel\Support\Arr; - -$array = ['name' => 'John Doe', 'age' => 17]; - -$exists = Arr::exists($array, 'name'); - -// true - -$exists = Arr::exists($array, 'salary'); - -// false -``` - - -#### `Arr::first()` - -The `Arr::first` method returns the first element of an array passing a given truth test: - -```php -use Hypervel\Support\Arr; - -$array = [100, 200, 300]; - -$first = Arr::first($array, function (int $value, int $key) { - return $value >= 150; -}); - -// 200 -``` - -A default value may also be passed as the third parameter to the method. This value will be returned if no value passes the truth test: - -```php -use Hypervel\Support\Arr; - -$first = Arr::first($array, $callback, $default); -``` - - -#### `Arr::flatten()` - -The `Arr::flatten` method flattens a multi-dimensional array into a single level array: - -```php -use Hypervel\Support\Arr; - -$array = ['name' => 'Joe', 'languages' => ['PHP', 'Ruby']]; - -$flattened = Arr::flatten($array); - -// ['Joe', 'PHP', 'Ruby'] -``` - - -#### `Arr::forget()` - -The `Arr::forget` method removes a given key / value pair from a deeply nested array using "dot" notation: - -```php -use Hypervel\Support\Arr; - -$array = ['products' => ['desk' => ['price' => 100]]]; - -Arr::forget($array, 'products.desk'); - -// ['products' => []] -``` - - -#### `Arr::get()` - -The `Arr::get` method retrieves a value from a deeply nested array using "dot" notation: - -```php -use Hypervel\Support\Arr; - -$array = ['products' => ['desk' => ['price' => 100]]]; - -$price = Arr::get($array, 'products.desk.price'); - -// 100 -``` - -The `Arr::get` method also accepts a default value, which will be returned if the specified key is not present in the array: - -```php -use Hypervel\Support\Arr; - -$discount = Arr::get($array, 'products.desk.discount', 0); - -// 0 -``` - - -#### `Arr::has()` - -The `Arr::has` method checks whether a given item or items exists in an array using "dot" notation: - -```php -use Hypervel\Support\Arr; - -$array = ['product' => ['name' => 'Desk', 'price' => 100]]; - -$contains = Arr::has($array, 'product.name'); - -// true - -$contains = Arr::has($array, ['product.price', 'product.discount']); - -// false -``` - - -#### `Arr::hasAny()` - -The `Arr::hasAny` method checks whether any item in a given set exists in an array using "dot" notation: - -```php -use Hypervel\Support\Arr; - -$array = ['product' => ['name' => 'Desk', 'price' => 100]]; - -$contains = Arr::hasAny($array, 'product.name'); - -// true - -$contains = Arr::hasAny($array, ['product.name', 'product.discount']); - -// true - -$contains = Arr::hasAny($array, ['category', 'product.discount']); - -// false -``` - - -#### `Arr::isAssoc()` - -The `Arr::isAssoc` method returns `true` if the given array is an associative array. An array is considered "associative" if it doesn't have sequential numerical keys beginning with zero: - -```php -use Hypervel\Support\Arr; - -$isAssoc = Arr::isAssoc(['product' => ['name' => 'Desk', 'price' => 100]]); - -// true - -$isAssoc = Arr::isAssoc([1, 2, 3]); - -// false -``` - - -#### `Arr::isList()` - -The `Arr::isList` method returns `true` if the given array's keys are sequential integers beginning from zero: - -```php -use Hypervel\Support\Arr; - -$isList = Arr::isList(['foo', 'bar', 'baz']); - -// true - -$isList = Arr::isList(['product' => ['name' => 'Desk', 'price' => 100]]); - -// false -``` - - -#### `Arr::join()` - -The `Arr::join` method joins array elements with a string. Using this method's second argument, you may also specify the joining string for the final element of the array: - -```php -use Hypervel\Support\Arr; - -$array = ['Tailwind', 'Alpine', 'Laravel', 'Hyperf']; - -$joined = Arr::join($array, ', '); - -// Tailwind, Alpine, Laravel, Hyperf - -$joined = Arr::join($array, ', ', ' and '); - -// Tailwind, Alpine, Laravel and Hyperf -``` - - -#### `Arr::keyBy()` - -The `Arr::keyBy` method keys the array by the given key. If multiple items have the same key, only the last one will appear in the new array: - -```php -use Hypervel\Support\Arr; - -$array = [ - ['product_id' => 'prod-100', 'name' => 'Desk'], - ['product_id' => 'prod-200', 'name' => 'Chair'], -]; - -$keyed = Arr::keyBy($array, 'product_id'); - -/* - [ - 'prod-100' => ['product_id' => 'prod-100', 'name' => 'Desk'], - 'prod-200' => ['product_id' => 'prod-200', 'name' => 'Chair'], - ] -*/ -``` - - -#### `Arr::last()` - -The `Arr::last` method returns the last element of an array passing a given truth test: - -```php -use Hypervel\Support\Arr; - -$array = [100, 200, 300, 110]; - -$last = Arr::last($array, function (int $value, int $key) { - return $value >= 150; -}); - -// 300 -``` - -A default value may be passed as the third argument to the method. This value will be returned if no value passes the truth test: - -```php -use Hypervel\Support\Arr; - -$last = Arr::last($array, $callback, $default); -``` - - -#### `Arr::map()` - -The `Arr::map` method iterates through the array and passes each value and key to the given callback. The array value is replaced by the value returned by the callback: - -```php -use Hypervel\Support\Arr; - -$array = ['first' => 'james', 'last' => 'kirk']; - -$mapped = Arr::map($array, function (string $value, string $key) { - return ucfirst($value); -}); - -// ['first' => 'James', 'last' => 'Kirk'] -``` - - -#### `Arr::mapWithKeys()` - -The `Arr::mapWithKeys` method iterates through the array and passes each value to the given callback. The callback should return an associative array containing a single key / value pair: - -```php -use Hypervel\Support\Arr; - -$array = [ - [ - 'name' => 'John', - 'department' => 'Sales', - 'email' => 'john@example.com', - ], - [ - 'name' => 'Jane', - 'department' => 'Marketing', - 'email' => 'jane@example.com', - ] -]; - -$mapped = Arr::mapWithKeys($array, function (array $item, int $key) { - return [$item['email'] => $item['name']]; -}); - -/* - [ - 'john@example.com' => 'John', - 'jane@example.com' => 'Jane', - ] -*/ -``` - - -#### `Arr::only()` - -The `Arr::only` method returns only the specified key / value pairs from the given array: - -```php -use Hypervel\Support\Arr; - -$array = ['name' => 'Desk', 'price' => 100, 'orders' => 10]; - -$slice = Arr::only($array, ['name', 'price']); - -// ['name' => 'Desk', 'price' => 100] -``` - - -#### `Arr::pluck()` - -The `Arr::pluck` method retrieves all of the values for a given key from an array: - -```php -use Hypervel\Support\Arr; - -$array = [ - ['developer' => ['id' => 1, 'name' => 'Taylor']], - ['developer' => ['id' => 2, 'name' => 'Abigail']], -]; - -$names = Arr::pluck($array, 'developer.name'); - -// ['Taylor', 'Abigail'] - -You may also specify how you wish the resulting list to be keyed: - -use Hypervel\Support\Arr; - -$names = Arr::pluck($array, 'developer.name', 'developer.id'); - -// [1 => 'Taylor', 2 => 'Abigail'] -``` - - -#### `Arr::prepend()` - -The `Arr::prepend` method will push an item onto the beginning of an array: - -```php -use Hypervel\Support\Arr; - -$array = ['one', 'two', 'three', 'four']; - -$array = Arr::prepend($array, 'zero'); - -// ['zero', 'one', 'two', 'three', 'four'] -``` - -If needed, you may specify the key that should be used for the value: - -```php -use Hypervel\Support\Arr; - -$array = ['price' => 100]; - -$array = Arr::prepend($array, 'Desk', 'name'); - -// ['name' => 'Desk', 'price' => 100] -``` - - -#### `Arr::prependKeysWith()` - -The `Arr::prependKeysWith` prepends all key names of an associative array with the given prefix: - -```php -use Hypervel\Support\Arr; - -$array = [ - 'name' => 'Desk', - 'price' => 100, -]; - -$keyed = Arr::prependKeysWith($array, 'product.'); - -/* - [ - 'product.name' => 'Desk', - 'product.price' => 100, - ] -*/ -``` - - -#### `Arr::pull()` - -The `Arr::pull` method returns and removes a key / value pair from an array: - -```php -use Hypervel\Support\Arr; - -$array = ['name' => 'Desk', 'price' => 100]; - -$name = Arr::pull($array, 'name'); - -// $name: Desk - -// $array: ['price' => 100] -``` - -A default value may be passed as the third argument to the method. This value will be returned if the key doesn't exist: - -```php -use Hypervel\Support\Arr; - -$value = Arr::pull($array, $key, $default); -``` - - -#### `Arr::query()` - -The `Arr::query` method converts the array into a query string: - -```php -use Hypervel\Support\Arr; - -$array = [ - 'name' => 'Taylor', - 'order' => [ - 'column' => 'created_at', - 'direction' => 'desc' - ] -]; - -Arr::query($array); - -// name=Taylor&order[column]=created_at&order[direction]=desc -``` - - -#### `Arr::random()` - -The `Arr::random` method returns a random value from an array: - -```php -use Hypervel\Support\Arr; - -$array = [1, 2, 3, 4, 5]; - -$random = Arr::random($array); - -// 4 - (retrieved randomly) -``` - -You may also specify the number of items to return as an optional second argument. Note that providing this argument will return an array even if only one item is desired: - -```php -use Hypervel\Support\Arr; - -$items = Arr::random($array, 2); - -// [2, 5] - (retrieved randomly) -``` - - -#### `Arr::set()` - -The `Arr::set` method sets a value within a deeply nested array using "dot" notation: - -```php -use Hypervel\Support\Arr; - -$array = ['products' => ['desk' => ['price' => 100]]]; - -Arr::set($array, 'products.desk.price', 200); - -// ['products' => ['desk' => ['price' => 200]]] -``` - - -#### `Arr::shuffle()` - -The `Arr::shuffle` method randomly shuffles the items in the array: - -```php -use Hypervel\Support\Arr; - -$array = Arr::shuffle([1, 2, 3, 4, 5]); - -// [3, 2, 5, 1, 4] - (generated randomly) -``` - - -#### `Arr::sort()` - -The `Arr::sort` method sorts an array by its values: - -```php -use Hypervel\Support\Arr; - -$array = ['Desk', 'Table', 'Chair']; - -$sorted = Arr::sort($array); - -// ['Chair', 'Desk', 'Table'] -``` - -You may also sort the array by the results of a given closure: - -```php -use Hypervel\Support\Arr; - -$array = [ - ['name' => 'Desk'], - ['name' => 'Table'], - ['name' => 'Chair'], -]; - -$sorted = array_values(Arr::sort($array, function (array $value) { - return $value['name']; -})); - -/* - [ - ['name' => 'Chair'], - ['name' => 'Desk'], - ['name' => 'Table'], - ] -*/ -``` - - -#### `Arr::sortDesc()` - -The `Arr::sortDesc` method sorts an array in descending order by its values: - -```php -use Hypervel\Support\Arr; - -$array = ['Desk', 'Table', 'Chair']; - -$sorted = Arr::sortDesc($array); - -// ['Table', 'Desk', 'Chair'] -``` - -You may also sort the array by the results of a given closure: - -```php -use Hypervel\Support\Arr; - -$array = [ - ['name' => 'Desk'], - ['name' => 'Table'], - ['name' => 'Chair'], -]; - -$sorted = array_values(Arr::sortDesc($array, function (array $value) { - return $value['name']; -})); - -/* - [ - ['name' => 'Table'], - ['name' => 'Desk'], - ['name' => 'Chair'], - ] -*/ -``` - - -#### `Arr::sortRecursive()` - -The `Arr::sortRecursive` method recursively sorts an array using the `sort` function for numerically indexed sub-arrays and the `ksort` function for associative sub-arrays: - -```php -use Hypervel\Support\Arr; - -$array = [ - ['Roman', 'Taylor', 'Li'], - ['PHP', 'Ruby', 'JavaScript'], - ['one' => 1, 'two' => 2, 'three' => 3], -]; - -$sorted = Arr::sortRecursive($array); - -/* - [ - ['JavaScript', 'PHP', 'Ruby'], - ['one' => 1, 'three' => 3, 'two' => 2], - ['Li', 'Roman', 'Taylor'], - ] -*/ -``` - -If you would like the results sorted in descending order, you may use the `Arr::sortRecursiveDesc` method. - -```php -$sorted = Arr::sortRecursiveDesc($array); -``` - - -#### `Arr::toCssClasses()` - -The `Arr::toCssClasses` method conditionally compiles a CSS class string. The method accepts an array of classes where the array key contains the class or classes you wish to add, while the value is a boolean expression. If the array element has a numeric key, it will always be included in the rendered class list: - -```php -use Hypervel\Support\Arr; - -$isActive = false; -$hasError = true; - -$array = ['p-4', 'font-bold' => $isActive, 'bg-red' => $hasError]; - -$classes = Arr::toCssClasses($array); - -/* - 'p-4 bg-red' -*/ -``` - - -#### `Arr::toCssStyles()` - -The `Arr::toCssStyles` conditionally compiles a CSS style string. The method accepts an array of classes where the array key contains the class or classes you wish to add, while the value is a boolean expression. If the array element has a numeric key, it will always be included in the rendered class list: - -```php -use Hypervel\Support\Arr; - -$hasColor = true; - -$array = ['background-color: blue', 'color: blue' => $hasColor]; - -$classes = Arr::toCssStyles($array); - -/* - 'background-color: blue; color: blue;' -*/ -``` - -This method powers Laravel's functionality allowing [merging classes with a Blade component's attribute bag](/docs/blade#conditionally-merge-classes) as well as the `@class` [Blade directive](/docs/blade#conditional-classes). - - -#### `Arr::undot()` - -The `Arr::undot` method expands a single-dimensional array that uses "dot" notation into a multi-dimensional array: - -```php -use Hypervel\Support\Arr; - -$array = [ - 'user.name' => 'Kevin Malone', - 'user.occupation' => 'Accountant', -]; - -$array = Arr::undot($array); - -// ['user' => ['name' => 'Kevin Malone', 'occupation' => 'Accountant']] -``` - - -#### `Arr::where()` - -The `Arr::where` method filters an array using the given closure: - -```php -use Hypervel\Support\Arr; - -$array = [100, '200', 300, '400', 500]; - -$filtered = Arr::where($array, function (string|int $value, int $key) { - return is_string($value); -}); - -// [1 => '200', 3 => '400'] -``` - - -#### `Arr::whereNotNull()` - -The `Arr::whereNotNull` method removes all `null` values from the given array: - -```php -use Hypervel\Support\Arr; - -$array = [0, null]; - -$filtered = Arr::whereNotNull($array); - -// [0 => 0] -``` - - -#### `Arr::wrap()` - -The `Arr::wrap` method wraps the given value in an array. If the given value is already an array it will be returned without modification: - -```php -use Hypervel\Support\Arr; - -$string = 'Laravel'; - -$array = Arr::wrap($string); - -// ['Laravel'] -``` - -If the given value is `null`, an empty array will be returned: - -```php -use Hypervel\Support\Arr; - -$array = Arr::wrap(null); - -// [] -``` - - -#### `data_fill()` - -The `data_fill` function sets a missing value within a nested array or object using "dot" notation: - -```php -$data = ['products' => ['desk' => ['price' => 100]]]; - -data_fill($data, 'products.desk.price', 200); - -// ['products' => ['desk' => ['price' => 100]]] - -data_fill($data, 'products.desk.discount', 10); - -// ['products' => ['desk' => ['price' => 100, 'discount' => 10]]] -``` - -This function also accepts asterisks as wildcards and will fill the target accordingly: - -```php -$data = [ - 'products' => [ - ['name' => 'Desk 1', 'price' => 100], - ['name' => 'Desk 2'], - ], -]; - -data_fill($data, 'products.*.price', 200); - -/* - [ - 'products' => [ - ['name' => 'Desk 1', 'price' => 100], - ['name' => 'Desk 2', 'price' => 200], - ], - ] -*/ -``` - - -#### `data_get()` - -The `data_get` function retrieves a value from a nested array or object using "dot" notation: - -```php -$data = ['products' => ['desk' => ['price' => 100]]]; - -$price = data_get($data, 'products.desk.price'); - -// 100 -``` - -The `data_get` function also accepts a default value, which will be returned if the specified key is not found: - -```php -$discount = data_get($data, 'products.desk.discount', 0); - -// 0 -``` - -The function also accepts wildcards using asterisks, which may target any key of the array or object: - -```php -$data = [ - 'product-one' => ['name' => 'Desk 1', 'price' => 100], - 'product-two' => ['name' => 'Desk 2', 'price' => 150], -]; - -data_get($data, '*.name'); - -// ['Desk 1', 'Desk 2']; -``` - - -#### `data_set()` - -The `data_set` function sets a value within a nested array or object using "dot" notation: - -```php -$data = ['products' => ['desk' => ['price' => 100]]]; - -data_set($data, 'products.desk.price', 200); - -// ['products' => ['desk' => ['price' => 200]]] -``` - -This function also accepts wildcards using asterisks and will set values on the target accordingly: - -```php -$data = [ - 'products' => [ - ['name' => 'Desk 1', 'price' => 100], - ['name' => 'Desk 2', 'price' => 150], - ], -]; - -data_set($data, 'products.*.price', 200); - -/* - [ - 'products' => [ - ['name' => 'Desk 1', 'price' => 200], - ['name' => 'Desk 2', 'price' => 200], - ], - ] -*/ -``` - -By default, any existing values are overwritten. If you wish to only set a value if it doesn't exist, you may pass `false` as the fourth argument to the function: - -```php -$data = ['products' => ['desk' => ['price' => 100]]]; - -data_set($data, 'products.desk.price', 200, overwrite: false); - -// ['products' => ['desk' => ['price' => 100]]] -``` - - -#### `data_forget()` - -The `data_forget` function removes a value within a nested array or object using "dot" notation: - -```php -$data = ['products' => ['desk' => ['price' => 100]]]; - -data_forget($data, 'products.desk.price'); - -// ['products' => ['desk' => []]] -``` - -This function also accepts wildcards using asterisks and will remove values on the target accordingly: - -```php -$data = [ - 'products' => [ - ['name' => 'Desk 1', 'price' => 100], - ['name' => 'Desk 2', 'price' => 150], - ], -]; - -data_forget($data, 'products.*.price'); - -/* - [ - 'products' => [ - ['name' => 'Desk 1'], - ['name' => 'Desk 2'], - ], - ] -*/ -``` - - -#### `head()` - -The `head` function returns the first element in the given array: - -```php -$array = [100, 200, 300]; - -$first = head($array); - -// 100 -``` - - -#### `last()` - -The `last` function returns the last element in the given array: - -```php -$array = [100, 200, 300]; - -$last = last($array); - -// 300 -``` - - -## Numbers - - -#### `Number::abbreviate()` - -The `Number::abbreviate` method returns the human-readable format of the provided numerical value, with an abbreviation for the units: - -```php -use Hypervel\Support\Number; - -$number = Number::abbreviate(1000); - -// 1K - -$number = Number::abbreviate(489939); - -// 490K - -$number = Number::abbreviate(1230000, precision: 2); - -// 1.23M -``` - - -#### `Number::clamp()` - -The `Number::clamp` method ensures a given number stays within a specified range. If the number is lower than the minimum, the minimum value is returned. If the number is higher than the maximum, the maximum value is returned: - -```php -use Hypervel\Support\Number; - -$number = Number::clamp(105, min: 10, max: 100); - -// 100 - -$number = Number::clamp(5, min: 10, max: 100); - -// 10 - -$number = Number::clamp(10, min: 10, max: 100); - -// 10 - -$number = Number::clamp(20, min: 10, max: 100); - -// 20 -``` - - -#### `Number::currency()` - -The `Number::currency` method returns the currency representation of the given value as a string: - -```php -use Hypervel\Support\Number; - -$currency = Number::currency(1000); - -// $1,000 - -$currency = Number::currency(1000, in: 'EUR'); - -// €1,000 - -$currency = Number::currency(1000, in: 'EUR', locale: 'de'); - -// 1.000 € -``` - - -#### `Number::fileSize()` - -The `Number::fileSize` method returns the file size representation of the given byte value as a string: - -```php -use Hypervel\Support\Number; - -$size = Number::fileSize(1024); - -// 1 KB - -$size = Number::fileSize(1024 * 1024); - -// 1 MB - -$size = Number::fileSize(1024, precision: 2); - -// 1.00 KB -``` - - -#### `Number::forHumans()` - -The `Number::forHumans` method returns the human-readable format of the provided numerical value: - -```php -use Hypervel\Support\Number; - -$number = Number::forHumans(1000); - -// 1 thousand - -$number = Number::forHumans(489939); - -// 490 thousand - -$number = Number::forHumans(1230000, precision: 2); - -// 1.23 million -``` - - -#### `Number::format()` - -The `Number::format` method formats the given number into a locale specific string: - -```php -use Hypervel\Support\Number; - -$number = Number::format(100000); - -// 100,000 - -$number = Number::format(100000, precision: 2); - -// 100,000.00 - -$number = Number::format(100000.123, maxPrecision: 2); - -// 100,000.12 - -$number = Number::format(100000, locale: 'de'); - -// 100.000 -``` - - -#### `Number::ordinal()` - -The `Number::ordinal` method returns a number's ordinal representation: - -```php -use Hypervel\Support\Number; - -$number = Number::ordinal(1); - -// 1st - -$number = Number::ordinal(2); - -// 2nd - -$number = Number::ordinal(21); - -// 21st -``` - - -#### `Number::percentage()` - -The `Number::percentage` method returns the percentage representation of the given value as a string: - -```php -use Hypervel\Support\Number; - -$percentage = Number::percentage(10); - -// 10% - -$percentage = Number::percentage(10, precision: 2); - -// 10.00% - -$percentage = Number::percentage(10.123, maxPrecision: 2); - -// 10.12% - -$percentage = Number::percentage(10, precision: 2, locale: 'de'); - -// 10,00% -``` - - -#### `Number::spell()` - -The `Number::spell` method transforms the given number into a string of words: - -```php -use Hypervel\Support\Number; - -$number = Number::spell(102); - -// one hundred and two - -$number = Number::spell(88, locale: 'fr'); - -// quatre-vingt-huit -``` - -The `after` argument allows you to specify a value after which all numbers should be spelled out: - -```php -$number = Number::spell(10, after: 10); - -// 10 - -$number = Number::spell(11, after: 10); - -// eleven -``` - -The `until` argument allows you to specify a value before which all numbers should be spelled out: - -```php -$number = Number::spell(5, until: 10); - -// five - -$number = Number::spell(10, until: 10); - -// 10 -``` - - -#### `Number::useLocale()` - -The `Number::useLocale` method sets the default number locale globally, which affects how numbers and currency are formatted by subsequent invocations to the `Number` class's methods: - -```php -use Hypervel\Support\Number; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Number::useLocale('de'); -} -``` - - -#### `Number::withLocale()` - -The `Number::withLocale` method executes the given closure using the specified locale and then restores the original locale after the callback has executed: - -```php -use Hypervel\Support\Number; - -$number = Number::withLocale('de', function () { - return Number::format(1500); -}); -``` - - -## Paths - - -#### `app_path()` - -The `app_path` function returns the fully qualified path to your application's `app` directory. You may also use the `app_path` function to generate a fully qualified path to a file relative to the application directory: - -```php -$path = app_path(); - -$path = app_path('Http/Controllers/Controller.php'); -``` - - -#### `base_path()` - -The `base_path` function returns the fully qualified path to your application's root directory. You may also use the `base_path` function to generate a fully qualified path to a given file relative to the project root directory: - -```php -$path = base_path(); - -$path = base_path('vendor/bin'); -``` - - -#### `config_path()` - -The `config_path` function returns the fully qualified path to your application's `config` directory. You may also use the `config_path` function to generate a fully qualified path to a given file within the application's configuration directory: - -```php -$path = config_path(); - -$path = config_path('app.php'); -``` - - -#### `database_path()` - -The `database_path` function returns the fully qualified path to your application's `database` directory. You may also use the `database_path` function to generate a fully qualified path to a given file within the database directory: - -```php -$path = database_path(); - -$path = database_path('factories/UserFactory.php'); -``` - - -#### `lang_path()` - -The `lang_path` function returns the fully qualified path to your application's `lang` directory. You may also use the `lang_path` function to generate a fully qualified path to a given file within the directory: - -```php -$path = lang_path(); - -$path = lang_path('en/messages.php'); -``` - - -#### `public_path()` - -The `public_path` function returns the fully qualified path to your application's `public` directory. You may also use the `public_path` function to generate a fully qualified path to a given file within the public directory: - -```php -$path = public_path(); - -$path = public_path('css/app.css'); -``` - - -#### `resource_path()` - -The `resource_path` function returns the fully qualified path to your application's `resources` directory. You may also use the `resource_path` function to generate a fully qualified path to a given file within the resources directory: - -```php -$path = resource_path(); - -$path = resource_path('sass/app.scss'); -``` - - -#### `storage_path()` - -The `storage_path` function returns the fully qualified path to your application's `storage` directory. You may also use the `storage_path` function to generate a fully qualified path to a given file within the storage directory: - -```php -$path = storage_path(); - -$path = storage_path('app/file.txt'); -``` - - -## URLs - - -#### `asset()` - -The `asset` function generates a URL for an asset using the current scheme of the request (HTTP or HTTPS): - -```php -$url = asset('img/photo.jpg'); -``` - -You can configure the asset URL host by setting the `ASSET_URL` variable in your `.env` file. This can be useful if you host your assets on an external service like Amazon S3 or another CDN: - -```php -// ASSET_URL=http://example.com/assets - -$url = asset('img/photo.jpg'); // http://example.com/assets/img/photo.jpg -``` - - -#### `route()` - -The `route` function generates a URL for a given [named route](/docs/routing#named-routes): - -```php -$url = route('route.name'); -``` - -If the route accepts parameters, you may pass them as the second argument to the function: - -```php -$url = route('route.name', ['id' => 1]); -``` - -By default, the `route` function generates an absolute URL. If you wish to generate a relative URL, you may pass `false` as the third argument to the function: - -```php -$url = route('route.name', ['id' => 1], false); -``` - - -#### `secure_asset()` - -The `secure_asset` function generates a URL for an asset using HTTPS: - -```php -$url = secure_asset('img/photo.jpg'); -``` - - -#### `secure_url()` - -The `secure_url` function generates a fully qualified HTTPS URL to the given path. Additional URL segments may be passed in the function's second argument: - -```php -$url = secure_url('user/profile'); - -$url = secure_url('user/profile', [1]); -``` - -#### `to_route()` - -The `to_route` function generates a PSR response for a given [named route](/docs/routing#named-routes): - -```php -return to_route('users.show', ['user' => 1]); -``` - -If necessary, you may pass the HTTP status code that should be assigned to the redirect and any additional response headers as the third and fourth arguments to the `to_route` method: - -```php -return to_route('users.show', ['user' => 1], 302, ['X-Framework' => 'Hypervel']); -``` - - -#### `url()` - -The `url` function generates a fully qualified URL to the given path: - - $url = url('user/profile'); - - $url = url('user/profile', [1]); - -If no path is provided, an `Hypervel\Router\UrlGenerator` instance is returned: - - $current = url()->current(); - - $full = url()->full(); - - $previous = url()->previous(); - - -#### `url()` - -The `url` function generates a fully qualified URL to the given path: - -```php -$url = url('user/profile'); - -$url = url('user/profile', [1]); -``` - -If no path is provided, an `Hypervel\Router\UrlGenerator` instance is returned: - -```php -$current = url()->current(); - -$full = url()->full(); - -$previous = url()->previous(); -``` - - -## Miscellaneous - - -#### `abort()` - -The `abort` function throws [an HTTP exception](/docs/errors#http-exceptions) which will be rendered by the [exception handler](/docs/errors#the-exception-handler): - -```php -abort(403); -``` - -You may also provide the exception's message and custom HTTP response headers that should be sent to the browser: - -```php -abort(403, 'Unauthorized.', $headers); -``` - - -#### `abort_if()` - -The `abort_if` function throws an HTTP exception if a given boolean expression evaluates to `true`: - -```php -abort_if(! Auth::user()->isAdmin(), 403); -``` - -Like the `abort` method, you may also provide the exception's response text as the third argument and an array of custom response headers as the fourth argument to the function. - - -#### `abort_unless()` - -The `abort_unless` function throws an HTTP exception if a given boolean expression evaluates to `false`: - -```php -abort_unless(Auth::user()->isAdmin(), 403); -``` - -Like the `abort` method, you may also provide the exception's response text as the third argument and an array of custom response headers as the fourth argument to the function. - - -#### `app()` - -The `app` function returns the [service container](/docs/container) instance: - -```php -$container = app(); -``` - -You may pass a class or interface name to resolve it from the container: - -```php -$api = app('HelpSpot\API'); -``` - - -#### `auth()` - -The `auth` function returns an [authenticator](/docs/authentication) instance. You may use it as an alternative to the `Auth` facade: - -```php -$user = auth()->user(); -``` - -If needed, you may specify which guard instance you would like to access: - -```php -$user = auth('admin')->user(); -``` - - -#### `bcrypt()` - -The `bcrypt` function [hashes](/docs/hashing) the given value using Bcrypt. You may use this function as an alternative to the `Hash` facade: - -```php -$password = bcrypt('my-secret-password'); -``` - - -#### `blank()` - -The `blank` function determines whether the given value is "blank": - -```php -blank(''); -blank(' '); -blank(null); -blank(collect()); - -// true - -blank(0); -blank(true); -blank(false); - -// false -``` - -For the inverse of `blank`, see the [`filled`](#method-filled) method. - - -#### `cache()` - -The `cache` function may be used to get values from the [cache](/docs/cache). If the given key does not exist in the cache, an optional default value will be returned: - -```php -$value = cache('key'); - -$value = cache('key', 'default'); -``` - -You may add items to the cache by passing an array of key / value pairs to the function. You should also pass the number of seconds or duration the cached value should be considered valid: - -```php -cache(['key' => 'value'], 300); - -cache(['key' => 'value'], now()->addSeconds(10)); -``` - - -#### `class_uses_recursive()` - -The `class_uses_recursive` function returns all traits used by a class, including traits used by all of its parent classes: - -```php -$traits = class_uses_recursive(App\Models\User::class); -``` - - -#### `collect()` - -The `collect` function creates a [collection](/docs/collections) instance from the given value: - -```php -$collection = collect(['taylor', 'abigail']); -``` - - -#### `config()` - -The `config` function gets the value of a [configuration](/docs/configuration) variable. The configuration values may be accessed using "dot" syntax, which includes the name of the file and the option you wish to access. A default value may be specified and is returned if the configuration option does not exist: - -```php -$value = config('app.timezone'); - -$value = config('app.timezone', $default); -``` - -You may set configuration variables at runtime by passing an array of key / value pairs. However, note that this function only affects the configuration value for the current request and does not update your actual configuration values: - -```php -config(['app.debug' => true]); -``` - - -#### `cookie()` - -The `cookie` function creates a new [cookie](/docs/requests#cookies) instance: - -```php -$cookie = cookie('name', 'value', $minutes); -``` - - -#### `decrypt()` - -The `decrypt` function [decrypts](/docs/encryption) the given value. You may use this function as an alternative to the `Crypt` facade: - -```php -$password = decrypt($value); -``` - - -#### `dd()` - -The `dd` function dumps the given variables and ends the execution of the script: - -```php -dd($value); - -dd($value1, $value2, $value3, ...); -``` - -If you do not want to halt the execution of your script, use the [`dump`](#method-dump) function instead. - - -#### `dispatch()` - -The `dispatch` function pushes the given [job](/docs/queues#creating-jobs) onto the Hypervel [job queue](/docs/queues): - -```php -dispatch(new App\Jobs\SendEmails); -``` - - -#### `dump()` - -The `dump` function dumps the given variables: - -```php -dump($value); - -dump($value1, $value2, $value3, ...); -``` - -If you want to stop executing the script after dumping the variables, use the [`dd`](#method-dd) function instead. - - -#### `encrypt()` - -The `encrypt` function [encrypts](/docs/encryption) the given value. You may use this function as an alternative to the `Crypt` facade: - -```php -$secret = encrypt('my-secret-value'); -``` - - -#### `env()` - -The `env` function retrieves the value of an [environment variable](/docs/configuration#environment-configuration) or returns a default value: - -```php -$env = env('APP_ENV'); - -$env = env('APP_ENV', 'production'); -``` - -::: warning -If you execute the `config:cache` command during your deployment process, you should be sure that you are only calling the `env` function from within your configuration files. Once the configuration has been cached, the `.env` file will not be loaded and all calls to the `env` function will return `null`. -::: - - -#### `event()` - -The `event` function dispatches the given [event](/docs/events) to its listeners: - -```php -event(new UserRegistered($user)); -``` - - -#### `filled()` - -The `filled` function determines whether the given value is not "blank": - -```php -filled(0); -filled(true); -filled(false); - -// true - -filled(''); -filled(' '); -filled(null); -filled(collect()); - -// false -``` - -For the inverse of `filled`, see the [`blank`](#method-blank) method. - - -#### `info()` - -The `info` function will write information to your application's [log](/docs/logging): - -```php -info('Some helpful information!'); -``` - -An array of contextual data may also be passed to the function: - -```php -info('User login attempt failed.', ['id' => $user->id]); -``` - - -#### `logger()` - -The `logger` function can be used to write a `debug` level message to the [log](/docs/logging): - -```php -logger('Debug message'); -``` - -An array of contextual data may also be passed to the function: - -```php -logger('User has logged in.', ['id' => $user->id]); -``` - -A [logger](/docs/errors#logging) instance will be returned if no value is passed to the function: - -```php -logger()->error('You are not allowed here.'); -``` - - -#### `method_field()` - -The `method_field` function generates an HTML `hidden` input field containing the spoofed value of the form's HTTP verb. For example, using [Blade syntax](/docs/blade): - -```html -
    - {{ method_field('DELETE') }} -
    -``` - - -#### `now()` - -The `now` function creates a new `Hypervel\Support\Carbon` instance for the current time: - -```php -$now = now(); -``` - - -#### `optional()` - -The `optional` function accepts any argument and allows you to access properties or call methods on that object. If the given object is `null`, properties and methods will return `null` instead of causing an error: - -```php -return optional($user->address)->street; - -{!! old('name', optional($user)->name) !!} -``` - -The `optional` function also accepts a closure as its second argument. The closure will be invoked if the value provided as the first argument is not null: - -```php -return optional(User::find($id), function (User $user) { - return $user->name; -}); -``` - - -#### `policy()` - -The `policy` method retrieves a [policy](/docs/authorization#creating-policies) instance for a given class: - -```php -$policy = policy(App\Models\User::class); -``` - - -#### `redirect()` - -The `redirect` function returns a [redirect HTTP response](/docs/responses#redirects), or returns the redirector instance if called with no arguments: - -```php -return redirect($to = null, $status = 302, $headers = [], $https = null); - -return redirect('/home'); - -return redirect()->route('route.name'); -``` - - -#### `report()` - -The `report` function will report an exception using your [exception handler](/docs/errors#the-exception-handler): - -```php -report($e); -``` - -The `report` function also accepts a string as an argument. When a string is given to the function, the function will create an exception with the given string as its message: - -```php -report('Something went wrong.'); -``` - - -#### `report_if()` - -The `report_if` function will report an exception using your [exception handler](/docs/errors#the-exception-handler) if the given condition is `true`: - -```php -report_if($shouldReport, $e); - -report_if($shouldReport, 'Something went wrong.'); -``` - - -#### `report_unless()` - -The `report_unless` function will report an exception using your [exception handler](/docs/errors#the-exception-handler) if the given condition is `false`: - -```php -report_unless($reportingDisabled, $e); - -report_unless($reportingDisabled, 'Something went wrong.'); -``` - - -#### `request()` - -The `request` function returns the current [request](/docs/requests) instance or obtains an input field's value from the current request: - -```php -$request = request(); - -$value = request('key', $default); -``` - - -#### `rescue()` - -The `rescue` function executes the given closure and catches any exceptions that occur during its execution. All exceptions that are caught will be sent to your [exception handler](/docs/errors#the-exception-handler); however, the request will continue processing: - -```php - return rescue(function () { - return $this->method(); - }); -``` - -You may also pass a second argument to the `rescue` function. This argument will be the "default" value that should be returned if an exception occurs while executing the closure: - -```php -return rescue(function () { - return $this->method(); -}, false); - -return rescue(function () { - return $this->method(); -}, function () { - return $this->failure(); -}); -``` - -A `report` argument may be provided to the `rescue` function to determine if the exception should be reported via the `report` function: - -```php -return rescue(function () { - return $this->method(); -}, report: function (Throwable $throwable) { - return $throwable instanceof InvalidArgumentException; -}); -``` - - -#### `resolve()` - -The `resolve` function resolves a given class or interface name to an instance using the [service container](/docs/container): - -```php -$api = resolve('HelpSpot\API'); -``` - - -#### `response()` - -The `response` function creates a [response](/docs/responses) instance or obtains an instance of the response factory: - -```php -return response('Hello World', 200, $headers); - -return response()->json(['foo' => 'bar'], 200, $headers); -``` - - -#### `retry()` - -The `retry` function attempts to execute the given callback until the given maximum attempt threshold is met. If the callback does not throw an exception, its return value will be returned. If the callback throws an exception, it will automatically be retried. If the maximum attempt count is exceeded, the exception will be thrown: - -```php -return retry(5, function () { - // Attempt 5 times while resting 100ms between attempts... -}, 100); -``` - -If you would like to manually calculate the number of milliseconds to sleep between attempts, you may pass a closure as the third argument to the `retry` function: - -```php -use Exception; - -return retry(5, function () { - // ... -}, function (int $attempt, Exception $exception) { - return $attempt * 100; -}); -``` - -For convenience, you may provide an array as the first argument to the `retry` function. This array will be used to determine how many milliseconds to sleep between subsequent attempts: - -```php -return retry([100, 200], function () { - // Sleep for 100ms on first retry, 200ms on second retry... -}); -``` - -To only retry under specific conditions, you may pass a closure as the fourth argument to the `retry` function: - -```php -use Exception; - -return retry(5, function () { - // ... -}, 100, function (Exception $exception) { - return $exception instanceof RetryException; -}); -``` - - -#### `session()` - -The `session` function may be used to get or set [session](/docs/session) values: - -```php -$value = session('key'); -``` - -You may set values by passing an array of key / value pairs to the function: - -```php -session(['chairs' => 7, 'instruments' => 3]); -``` - -The session store will be returned if no value is passed to the function: - -```php -$value = session()->get('key'); - -session()->put('key', $value); -``` - - -#### `tap()` - -The `tap` function accepts two arguments: an arbitrary `$value` and a closure. The `$value` will be passed to the closure and then be returned by the `tap` function. The return value of the closure is irrelevant: - -```php -$user = tap(User::first(), function (User $user) { - $user->name = 'taylor'; - - $user->save(); -}); -``` - -If no closure is passed to the `tap` function, you may call any method on the given `$value`. The return value of the method you call will always be `$value`, regardless of what the method actually returns in its definition. For example, the Eloquent `update` method typically returns an integer. However, we can force the method to return the model itself by chaining the `update` method call through the `tap` function: - -```php -$user = tap($user)->update([ - 'name' => $name, - 'email' => $email, -]); -``` - -To add a `tap` method to a class, you may add the `Hypervel\Support\Traits\Tappable` trait to the class. The `tap` method of this trait accepts a Closure as its only argument. The object instance itself will be passed to the Closure and then be returned by the `tap` method: - -```php -return $user->tap(function (User $user) { - // ... -}); -``` - - -#### `throw_if()` - -The `throw_if` function throws the given exception if a given boolean expression evaluates to `true`: - -```php -throw_if(! Auth::user()->isAdmin(), AuthorizationException::class); - -throw_if( - ! Auth::user()->isAdmin(), - AuthorizationException::class, - 'You are not allowed to access this page.' -); -``` - - -#### `throw_unless()` - -The `throw_unless` function throws the given exception if a given boolean expression evaluates to `false`: - -```php -throw_unless(Auth::user()->isAdmin(), AuthorizationException::class); - -throw_unless( - Auth::user()->isAdmin(), - AuthorizationException::class, - 'You are not allowed to access this page.' -); -``` - - -#### `today()` - -The `today` function creates a new `Hypervel\Support\Carbon` instance for the current date: - -```php -$today = today(); -``` - - -#### `trait_uses_recursive()` - -The `trait_uses_recursive` function returns all traits used by a trait: - -```php -$traits = trait_uses_recursive(\Hypervel\Notifications\Notifiable::class); -``` - - -#### `transform()` - -The `transform` function executes a closure on a given value if the value is not [blank](#method-blank) and then returns the return value of the closure: - -```php -$callback = function (int $value) { - return $value * 2; -}; - -$result = transform(5, $callback); - -// 10 -``` - -A default value or closure may be passed as the third argument to the function. This value will be returned if the given value is blank: - -```php -$result = transform(null, $callback, 'The value is blank'); - -// The value is blank -``` - - -#### `validator()` - -The `validator` function creates a new [validator](/docs/validation) instance with the given arguments. You may use it as an alternative to the `Validator` facade: - -```php -$validator = validator($data, $rules, $messages); -``` - - -#### `value()` - -The `value` function returns the value it is given. However, if you pass a closure to the function, the closure will be executed and its returned value will be returned: - -```php -$result = value(true); - -// true - -$result = value(function () { - return false; -}); - -// false -``` - -Additional arguments may be passed to the `value` function. If the first argument is a closure then the additional parameters will be passed to the closure as arguments, otherwise they will be ignored: - -```php -$result = value(function (string $name) { - return $name; -}, 'Taylor'); - -// 'Taylor' -``` - - -#### `view()` - -The `view` function retrieves a [view](/docs/views) instance: - -```php -return view('auth.login'); -``` - - -#### `with()` - -The `with` function returns the value it is given. If a closure is passed as the second argument to the function, the closure will be executed and its returned value will be returned: - -```php -$callback = function (mixed $value) { - return is_numeric($value) ? $value * 2 : 0; -}; - -$result = with(5, $callback); - -// 10 - -$result = with(null, $callback); - -// 0 - -$result = with(5, null); - -// 5 -``` - -## Other Utilities - -### Dates - -Hypervel includes [Carbon](https://carbon.nesbot.com/docs/), a powerful date and time manipulation library. To create a new `Carbon` instance, you may invoke the `now` function. This function is globally available within your Hypervel application: - -```php -$now = now(); -``` - -Or, you may create a new `Carbon` instance using the `Hypervel\Support\Carbon` class: - -```php -use Hypervel\Support\Carbon; - -$now = Carbon::now(); -``` - -For a thorough discussion of Carbon and its features, please consult the [official Carbon documentation](https://carbon.nesbot.com/docs/). - -### Pipeline - -Hypervel's `Pipeline` provides a convenient way to "pipe" a given input through a series of invokable classes, closures, or callables, giving each class the opportunity to inspect or modify the input and invoke the next callable in the pipeline: - -```php -use Closure; -use App\Models\User; -use Hypervel\Support\Pipeline; - -$user = Pipeline::make() - ->send($user) - ->through([ - function (User $user, Closure $next) { - // ... - - return $next($user); - }, - function (User $user, Closure $next) { - // ... - - return $next($user); - }, - ]) - ->then(fn (User $user) => $user); -``` - -As you can see, each invokable class or closure in the pipeline is provided the input and a `$next` closure. Invoking the `$next` closure will invoke the next callable in the pipeline. As you may have noticed, this is very similar to [middleware](/docs/middleware). - -When the last callable in the pipeline invokes the `$next` closure, the callable provided to the `then` method will be invoked. Typically, this callable will simply return the given input. - -Of course, as discussed previously, you are not limited to providing closures to your pipeline. You may also provide invokable classes. If a class name is provided, the class will be instantiated via Hypervel's [service container](/docs/container), allowing dependencies to be injected into the invokable class: - -```php -$user = Pipeline::make() - ->send($user) - ->through([ - GenerateProfilePhoto::class, - ActivateSubscription::class, - SendWelcomeEmail::class, - ]) - ->then(fn (User $user) => $user); -``` - -### Sleep - -Hypervel's `Sleep` class is a light-weight wrapper around PHP's native `sleep` and `usleep` functions, offering greater testability while also exposing a developer friendly API for working with time: - -```php -use Hypervel\Support\Sleep; - -$waiting = true; - -while ($waiting) { - Sleep::for(1)->second(); - - $waiting = /* ... */; -} -``` - -The `Sleep` class offers a variety of methods that allow you to work with different units of time: - -```php -// Return a value after sleeping... -$result = Sleep::for(1)->second()->then(fn () => 1 + 1); - -// Sleep while a given value is true... -Sleep::for(1)->second()->while(fn () => shouldKeepSleeping()); - -// Pause execution for 90 seconds... -Sleep::for(1.5)->minutes(); - -// Pause execution for 2 seconds... -Sleep::for(2)->seconds(); - -// Pause execution for 500 milliseconds... -Sleep::for(500)->milliseconds(); - -// Pause execution for 5,000 microseconds... -Sleep::for(5000)->microseconds(); - -// Pause execution until a given time... -Sleep::until(now()->addMinute()); - -// Alias of PHP's native "sleep" function... -Sleep::sleep(2); - -// Alias of PHP's native "usleep" function... -Sleep::usleep(5000); -``` - -To easily combine units of time, you may use the `and` method: - -```php -Sleep::for(1)->second()->and(10)->milliseconds(); -``` - -#### Testing Sleep - -When testing code that utilizes the `Sleep` class or PHP's native sleep functions, your test will pause execution. As you might expect, this makes your test suite significantly slower. For example, imagine you are testing the following code: - -```php -$waiting = /* ... */; - -$seconds = 1; - -while ($waiting) { - Sleep::for($seconds++)->seconds(); - - $waiting = /* ... */; -} -``` - -Typically, testing this code would take at least one second. Luckily, the `Sleep` class allows us to "fake" sleeping so that our test suite stays fast: - -```php -public function testItWaitsUntilReady() -{ - Sleep::fake(); - - // ... -} -``` - -When faking the `Sleep` class, the actual execution pause is bypassed, leading to a substantially faster test. - -Once the `Sleep` class has been faked, it is possible to make assertions against the expected "sleeps" that should have occurred. To illustrate this, let's imagine we are testing code that pauses execution three times, with each pause increasing by a single second. Using the `assertSequence` method, we can assert that our code "slept" for the proper amount of time while keeping our test fast: - -```php -public function testItChecksIfReadyThreeTimes() -{ - Sleep::fake(); - - // ... - - Sleep::assertSequence([ - Sleep::for(1)->second(), - Sleep::for(2)->seconds(), - Sleep::for(3)->seconds(), - ]); -} -``` - -Of course, the `Sleep` class offers a variety of other assertions you may use when testing: - -```php -use Carbon\CarbonInterval as Duration; -use Hypervel\Support\Sleep; - -// Assert that sleep was called 3 times... -Sleep::assertSleptTimes(3); - -// Assert against the duration of sleep... -Sleep::assertSlept(function (Duration $duration): bool { - return /* ... */; -}, times: 1); - -// Assert that the Sleep class was never invoked... -Sleep::assertNeverSlept(); - -// Assert that, even if Sleep was called, no execution paused occurred... -Sleep::assertInsomniac(); -``` - -Sometimes it may be useful to perform an action whenever a fake sleep occurs. To achieve this, you may provide a callback to the `whenFakingSleep` method. In the following example, we use Hypervel's [time manipulation helpers](/docs/mocking#interacting-with-time) to instantly progress time by the duration of each sleep: - -```php -use Carbon\CarbonInterval as Duration; - -$this->freezeTime(); - -Sleep::fake(); - -Sleep::whenFakingSleep(function (Duration $duration) { - // Progress time when faking sleep... - $this->travel($duration->totalMilliseconds)->milliseconds(); -}); -``` - -As progressing time is a common requirement, the fake method accepts a `syncWithCarbon` argument to keep Carbon in sync when sleeping within a test: - -```php -Sleep::fake(syncWithCarbon: true); - -$start = now(); - -Sleep::for(1)->second(); - -$start->diffForHumans(); // 1 second ago -``` - -Hypervel uses the `Sleep` class internally whenever it is pausing execution. For example, the [retry](/docs/helpers#method-retry) helper uses the `Sleep` class when sleeping, allowing for improved testability when using that helper. \ No newline at end of file diff --git a/_archive/docs/0.3/docs/horizon.md b/_archive/docs/0.3/docs/horizon.md deleted file mode 100644 index e693e9418e..0000000000 --- a/_archive/docs/0.3/docs/horizon.md +++ /dev/null @@ -1,660 +0,0 @@ -# Hypervel Horizon -[[toc]] - -## Introduction - -::: note -Before digging into Hypervel Horizon, you should familiarize yourself with Hypervel's base [queue services](/docs/queues). Horizon augments Hypervel's queue with additional features that may be confusing if you are not already familiar with the basic queue features offered by Hypervel. -::: - -[Hypervel Horizon](https://github.com/hypervel/horizon) provides a beautiful dashboard and code-driven configuration for your Hypervel powered [Redis queues](/docs/queues). Horizon allows you to easily monitor key metrics of your queue system such as job throughput, runtime, and job failures. - -When using Horizon, all of your queue worker configuration is stored in a single, simple configuration file. By defining your application's worker configuration in a version controlled file, you may easily scale or modify your application's queue workers when deploying your application. - - - -## Installation - -::: warning -Hypervel Horizon requires that you use [Redis](https://redis.io) to power your queue. Therefore, you should ensure that your queue connection is set to `redis` in your application's `config/queue.php` configuration file. -::: - -You may install Horizon into your project using the Composer package manager: - -```shell -composer require hypervel/horizon -``` - -After installing Horizon, publish its assets using the `horizon:install` Artisan command: - -```shell -php artisan horizon:install -``` - -### Configuration - -After publishing Horizon's assets, its primary configuration file will be located at `config/horizon.php`. This configuration file allows you to configure the queue worker options for your application. Each configuration option includes a description of its purpose, so be sure to thoroughly explore this file. - -::: warning -Horizon uses a Redis connection named `horizon` internally. This Redis connection name is reserved and should not be assigned to another Redis connection in the `database.php` configuration file or as the value of the `use` option in the `horizon.php` configuration file. -::: - -#### Environments - -After installation, the primary Horizon configuration option that you should familiarize yourself with is the `environments` configuration option. This configuration option is an array of environments that your application runs on and defines the worker process options for each environment. By default, this entry contains a `production` and `local` environment. However, you are free to add more environments as needed: - -```php -'environments' => [ - 'production' => [ - 'supervisor-1' => [ - 'maxProcesses' => 10, - 'balanceMaxShift' => 1, - 'balanceCooldown' => 3, - ], - ], - - 'local' => [ - 'supervisor-1' => [ - 'maxProcesses' => 3, - ], - ], -], -``` - -You may also define a wildcard environment (`*`) which will be used when no other matching environment is found: - -```php -'environments' => [ - // ... - - '*' => [ - 'supervisor-1' => [ - 'maxProcesses' => 3, - ], - ], -], -``` - -When you start Horizon, it will use the worker process configuration options for the environment that your application is running on. Typically, the environment is determined by the value of the `APP_ENV` [environment variable](/docs/configuration#determining-the-current-environment). For example, the default `local` Horizon environment is configured to start three worker processes and automatically balance the number of worker processes assigned to each queue. The default `production` environment is configured to start a maximum of 10 worker processes and automatically balance the number of worker processes assigned to each queue. - -::: warning -You should ensure that the `environments` portion of your `horizon` configuration file contains an entry for each [environment](/docs/configuration#environment-configuration) on which you plan to run Horizon. -::: - -#### Supervisors - -As you can see in Horizon's default configuration file, each environment can contain one or more "supervisors". By default, the configuration file defines this supervisor as `supervisor-1`; however, you are free to name your supervisors whatever you want. Each supervisor is essentially responsible for "supervising" a group of worker processes and takes care of balancing worker processes across queues. - -You may add additional supervisors to a given environment if you would like to define a new group of worker processes that should run in that environment. You may choose to do this if you would like to define a different balancing strategy or worker process count for a given queue used by your application. - -#### Default Values - -Within Horizon's default configuration file, you will notice a `defaults` configuration option. This configuration option specifies the default values for your application's [supervisors](#supervisors). The supervisor's default configuration values will be merged into the supervisor's configuration for each environment, allowing you to avoid unnecessary repetition when defining your supervisors. - -### Dashboard Authorization - -The Horizon dashboard may be accessed via the `/horizon` route. By default, you will only be able to access this dashboard in the `local` environment. However, within your `app/Providers/HorizonServiceProvider.php` file, there is an [authorization gate](/docs/authorization#gates) definition. This authorization gate controls access to Horizon in **non-local** environments. You are free to modify this gate as needed to restrict access to your Horizon installation: - -```php -/** - * Register the Horizon gate. - * - * This gate determines who can access Horizon in non-local environments. - */ -protected function gate(): void -{ - Gate::define('viewHorizon', function (User $user) { - return in_array($user->email, [ - 'taylor@Hypervel.com', - ]); - }); -} -``` - -#### Alternative Authentication Strategies - -Remember that Hypervel automatically injects the authenticated user into the gate closure. If your application is providing Horizon security via another method, such as IP restrictions, then your Horizon users may not need to "login". Therefore, you will need to change `function (User $user)` closure signature above to `function (User $user = null)` in order to force Hypervel to not require authentication. - -### Max Job Attempts - -::: note -Before refining these options, make sure you are familiar with Hypervel's default [queue services](/docs/queues#max-job-attempts-and-timeout) and the concept of 'attempts'. -::: - -You can define the maximum number of attempts a job can consume within a supervisor's configuration: - -```php -'environments' => [ - 'production' => [ - 'supervisor-1' => [ - // ... - 'tries' => 10, - ], - ], -], -``` - -::: note -This option is similar to the `--tries` option when using the Artisan command to process queues. -::: - -Adjusting the `tries` option is essential when using middlewares such as `WithoutOverlapping` or `RateLimited` because they consume attempts. To handle this, adjust the `tries` configuration value either at the supervisor level or by defining the `$tries` property on the job class. - -If you don't set the `tries` option, Horizon defaults to a single attempt, unless the job class defines `$tries`, which takes precedence over the Horizon configuration. - -Setting `tries` or `$tries` to 0 allows unlimited attempts, which is ideal when the number of attempts is uncertain. To prevent endless failures, you can limit the number of exceptions allowed by setting the `$maxExceptions` property on the job class. - -### Job Timeout - -Similarly, you can set a `timeout` value at the supervisor level, which specifies how many seconds a worker process can run a job before it's forcefully terminated. Once terminated, the job will either be retried or marked as failed, depending on your queue configuration: - -```php -'environments' => [ - 'production' => [ - 'supervisor-1' => [ - // ...¨ - 'timeout' => 60, - ], - ], -], -``` - -::: warning -When using the `auto` balancing strategy, Horizon will consider in-progress workers as "hanging" and force-kill them after the Horizon timeout during scale down. Always ensure the Horizon timeout is greater than any job-level timeout, otherwise jobs may be terminated mid-execution. In addition, the `timeout` value should always be at least a few seconds shorter than the `retry_after` value defined in your `config/queue.php` configuration file. Otherwise, your jobs may be processed twice. -::: - -### Job Backoff - -You can define the `backoff` value at the supervisor level to specify how long Horizon should wait before retrying a job that encounters an unhandled exception: - -```php -'environments' => [ - 'production' => [ - 'supervisor-1' => [ - // ... - 'backoff' => 10, - ], - ], -], -``` - -You may also configure "exponential" backoffs by using an array for the `backoff` value. In this example, the retry delay will be 1 second for the first retry, 5 seconds for the second retry, 10 seconds for the third retry, and 10 seconds for every subsequent retry if there are more attempts remaining: - -```php -'environments' => [ - 'production' => [ - 'supervisor-1' => [ - // ... - 'backoff' => [1, 5, 10], - ], - ], -], -``` - -### Silenced Jobs - -Sometimes, you may not be interested in viewing certain jobs dispatched by your application or third-party packages. Instead of these jobs taking up space in your "Completed Jobs" list, you can silence them. To get started, add the job's class name to the `silenced` configuration option in your application's `horizon` configuration file: - -```php -'silenced' => [ - App\Jobs\ProcessPodcast::class, -], -``` - -In addition to silencing individual job classes, Horizon also supports silencing jobs based on [tags](#tags). This can be useful if you want to hide multiple jobs that share a common tag: - -```php -'silenced_tags' => [ - 'notifications' -], -``` - -Alternatively, the job you wish to silence can implement the `Hypervel\Horizon\Contracts\Silenced` interface. If a job implements this interface, it will automatically be silenced, even if it is not present in the `silenced` configuration array: - -```php -use Hypervel\Horizon\Contracts\Silenced; - -class ProcessPodcast implements ShouldQueue, Silenced -{ - use Queueable; - - // ... -} -``` - -## Balancing Strategies - -Each supervisor can process one or more queues but unlike Hypervel's default queue system, Horizon allows you to choose from three worker balancing strategies: `auto`, `simple`, and `false`. - -::: important -Unlike Laravel, Hypervel's Queue Worker has native concurrency capabilities. In traditional Laravel, even when handling multiple I/O-bound tasks, you need multiple workers to achieve concurrency and avoid blocking. However, in Hypervel, you can easily specify the concurrency level for each worker through the `--concurrency` parameter, which utilizes coroutines for concurrent execution. - -This means that what previously required 10 worker processes in Laravel to achieve a certain level of concurrency can now be accomplished in Hypervel with just a single worker by controlling the number of concurrent coroutines through the `concurrency` parameter! - -```php -php artisan horizon:supervisor --concurrency=10 -``` - -> If you have 10 workers and set the concurrency to 10, this means you can have a total concurrency capacity of 100. -::: - -### Auto Balancing - -The `auto` strategy, which is the default strategy, adjusts the number of worker processes per queue based on the current workload of the queue. For example, if your `notifications` queue has 1,000 pending jobs while your `default` queue is empty, Horizon will allocate more workers to your `notifications` queue until the queue is empty. - -When using the `auto` strategy, you may also configure the `minProcesses` and `maxProcesses` configuration options: - -
    - -- `minProcesses` defines the minimum number of worker processes per queue. This value must be greater than or equal to 1. -- `maxProcesses` defines the maximum total number of worker processes Horizon may scale up to across all queues. This value should typically be greater than the number of queues multiplied by the `minProcesses` value. To prevent the supervisor from spawning any processes, you may set this value to 0. - -
    - -For example, you may configure Horizon to maintain at least one process per queue and scale up to a total of 10 worker processes: - -```php -'environments' => [ - 'production' => [ - 'supervisor-1' => [ - 'connection' => 'redis', - 'queue' => ['default', 'notifications'], - 'balance' => 'auto', - 'autoScalingStrategy' => 'time', - 'minProcesses' => 1, - 'maxProcesses' => 10, - 'balanceMaxShift' => 1, - 'balanceCooldown' => 3, - ], - ], -], -``` - -The `autoScalingStrategy` configuration option determines how Horizon will assign more worker processes to queues. You can choose between two strategies: - -
    - -- The `time` strategy will assign workers based on the total estimated amount of time it will take to clear the queue. -- The `size` strategy will assign workers based on the total number of jobs on the queue. - -
    - -The `balanceMaxShift` and `balanceCooldown` configuration values determine how quickly Horizon will scale to meet worker demand. In the example above, a maximum of one new process will be created or destroyed every three seconds. You are free to tweak these values as necessary based on your application's needs. - -#### Queue Priorities and Auto Balancing - -When using the `auto` balancing strategy, Horizon does not enforce strict priority between queues. The order of queues in a supervisor's configuration does not affect how worker processes are assigned. Instead, Horizon relies on the selected `autoScalingStrategy` to dynamically allocate worker processes based on queue load. - -For example, in the following configuration, the high queue is not prioritized over the default queue, despite appearing first in the list: - -```php -'environments' => [ - 'production' => [ - 'supervisor-1' => [ - // ... - 'queue' => ['high', 'default'], - 'minProcesses' => 1, - 'maxProcesses' => 10, - ], - ], -], -``` - -If you need to enforce a relative priority between queues, you may define multiple supervisors and explicitly allocate processing resources: - -```php -'environments' => [ - 'production' => [ - 'supervisor-1' => [ - // ... - 'queue' => ['default'], - 'minProcesses' => 1, - 'maxProcesses' => 10, - ], - 'supervisor-2' => [ - // ... - 'queue' => ['images'], - 'minProcesses' => 1, - 'maxProcesses' => 1, - ], - ], -], -``` - -In this example, the default `queue` can scale up to 10 processes, while the `images` queue is limited to one process. This configuration ensures that your queues can scale independently. - -::: note -When dispatching resource-intensive jobs, it's sometimes best to assign them to a dedicated queue with a limited `maxProcesses` value. Otherwise, these jobs could consume excessive CPU resources and overload your system. -::: - -### Simple Balancing - -The `simple` strategy distributes worker processes evenly across the specified queues. With this strategy, Horizon does not automatically scale the number of worker processes. Rather, it uses a fixed number of processes: - -```php -'environments' => [ - 'production' => [ - 'supervisor-1' => [ - // ... - 'queue' => ['default', 'notifications'], - 'balance' => 'simple', - 'processes' => 10, - ], - ], -], -``` - -In the example above, Horizon will assign 5 processes to each queue, splitting the total of 10 evenly. - -If you'd like to control the number of worker processes assigned to each queue individually, you can define multiple supervisors: - -```php -'environments' => [ - 'production' => [ - 'supervisor-1' => [ - // ... - 'queue' => ['default'], - 'balance' => 'simple', - 'processes' => 10, - ], - 'supervisor-notifications' => [ - // ... - 'queue' => ['notifications'], - 'balance' => 'simple', - 'processes' => 2, - ], - ], -], -``` - -With this configuration, Horizon will assign 10 processes to the `default` queue and 2 processes to the `notifications` queue. - -### No Balancing - -When the `balance` option is set to `false`, Horizon processes queues strictly in the order they're listed, similar to Hypervel's default queue system. However, it will still scale the number of worker processes if jobs begin to accumulate: - -```php -'environments' => [ - 'production' => [ - 'supervisor-1' => [ - // ... - 'queue' => ['default', 'notifications'], - 'balance' => false, - 'minProcesses' => 1, - 'maxProcesses' => 10, - ], - ], -], -``` - -In the example above, jobs in the `default` queue are always prioritized over jobs in the `notifications` queue. For instance, if there are 1,000 jobs in `default` and only 10 in `notifications`, Horizon will fully process all `default` jobs before handling any from `notifications`. - -You can control Horizon's ability to scale worker processes using the `minProcesses` and `maxProcesses` options: - -
    - -- `minProcesses` defines the minimum number of worker processes in total. This value must be greater than or equal to 1. -- `maxProcesses` defines the maximum total number of worker processes Horizon may scale up to. - -
    - -## Running Horizon - -Once you have configured your supervisors and workers in your application's `config/horizon.php` configuration file, you may start Horizon using the `horizon` Artisan command. This single command will start all of the configured worker processes for the current environment: - -```shell -php artisan horizon -``` - -You may pause the Horizon process and instruct it to continue processing jobs using the `horizon:pause` and `horizon:continue` Artisan commands: - -```shell -php artisan horizon:pause - -php artisan horizon:continue -``` - -You may also pause and continue specific Horizon [supervisors](#supervisors) using the `horizon:pause-supervisor` and `horizon:continue-supervisor` Artisan commands: - -```shell -php artisan horizon:pause-supervisor supervisor-1 - -php artisan horizon:continue-supervisor supervisor-1 -``` - -You may check the current status of the Horizon process using the `horizon:status` Artisan command: - -```shell -php artisan horizon:status -``` - -You may check the current status of a specific Horizon [supervisor](#supervisors) using the `horizon:supervisor-status` Artisan command: - -```shell -php artisan horizon:supervisor-status supervisor-1 -``` - -You may gracefully terminate the Horizon process using the `horizon:terminate` Artisan command. Any jobs that are currently being processed will be completed and then Horizon will stop executing: - -```shell -php artisan horizon:terminate -``` - -### Deploying Horizon - -When you're ready to deploy Horizon to your application's actual server, you should configure a process monitor to monitor the `php artisan horizon` command and restart it if it exits unexpectedly. Don't worry, we'll discuss how to install a process monitor below. - -During your application's deployment process, you should instruct the Horizon process to terminate so that it will be restarted by your process monitor and receive your code changes: - -```shell -php artisan horizon:terminate -``` - -#### Installing Supervisor - -Supervisor is a process monitor for the Linux operating system and will automatically restart your `horizon` process if it stops executing. To install Supervisor on Ubuntu, you may use the following command. If you are not using Ubuntu, you can likely install Supervisor using your operating system's package manager: - -```shell -sudo apt-get install supervisor -``` - -#### Supervisor Configuration - -Supervisor configuration files are typically stored within your server's `/etc/supervisor/conf.d` directory. Within this directory, you may create any number of configuration files that instruct supervisor how your processes should be monitored. For example, let's create a `horizon.conf` file that starts and monitors a `horizon` process: - -```ini -[program:horizon] -process_name=%(program_name)s -command=php /home/forge/example.com/artisan horizon -autostart=true -autorestart=true -user=forge -redirect_stderr=true -stdout_logfile=/home/forge/example.com/horizon.log -stopwaitsecs=3600 -``` - -When defining your Supervisor configuration, you should ensure that the value of `stopwaitsecs` is greater than the number of seconds consumed by your longest running job. Otherwise, Supervisor may kill the job before it is finished processing. - -::: warning -While the examples above are valid for Ubuntu based servers, the location and file extension expected of Supervisor configuration files may vary between other server operating systems. Please consult your server's documentation for more information. -::: - -#### Starting Supervisor - -Once the configuration file has been created, you may update the Supervisor configuration and start the monitored processes using the following commands: - -```shell -sudo supervisorctl reread - -sudo supervisorctl update - -sudo supervisorctl start horizon -``` - -::: note -For more information on running Supervisor, consult the [Supervisor documentation](http://supervisord.org/index.html). -::: - -## Tags - -Horizon allows you to assign "tags" to jobs, including mailables, broadcast events, notifications, and queued event listeners. In fact, Horizon will intelligently and automatically tag most jobs depending on the Eloquent models that are attached to the job. For example, take a look at the following job: - -```php - - */ - public function tags(): array - { - return ['render', 'video:'.$this->video->id]; - } -} -``` - -#### Manually Tagging Event Listeners - -When retrieving the tags for a queued event listener, Horizon will automatically pass the event instance to the `tags` method, allowing you to add event data to the tags: - -```php -class SendRenderNotifications implements ShouldQueue -{ - /** - * Get the tags that should be assigned to the listener. - * - * @return array - */ - public function tags(VideoRendered $event): array - { - return ['video:'.$event->video->id]; - } -} -``` - -## Notifications - -::: warning -When configuring Horizon to send Slack or SMS notifications, you should review the [prerequisites for the relevant notification channel](/docs/notifications). -::: - -If you would like to be notified when one of your queues has a long wait time, you may use the `Horizon::routeMailNotificationsTo`, `Horizon::routeSlackNotificationsTo`, and `Horizon::routeSmsNotificationsTo` methods. You may call these methods from the `boot` method of your application's `App\Providers\HorizonServiceProvider`: - -```php -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - parent::boot(); - - Horizon::routeSmsNotificationsTo('15556667777'); - Horizon::routeMailNotificationsTo('example@example.com'); - Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel'); -} -``` - -#### Configuring Notification Wait Time Thresholds - -You may configure how many seconds are considered a "long wait" within your application's `config/horizon.php` configuration file. The `waits` configuration option within this file allows you to control the long wait threshold for each connection / queue combination. Any undefined connection / queue combinations will default to a long wait threshold of 60 seconds: - -```php -'waits' => [ - 'redis:critical' => 30, - 'redis:default' => 60, - 'redis:batch' => 120, -], -``` - -Setting a queue's threshold to `0` will disable long wait notifications for that queue. - -## Metrics - -Horizon includes a metrics dashboard which provides information regarding your job and queue wait times and throughput. In order to populate this dashboard, you should configure Horizon's `snapshot` Artisan command to run every five minutes in your application's `routes/console.php` file: - -```php -use Hypervel\Support\Facades\Schedule; - -Schedule::command('horizon:snapshot')->everyFiveMinutes(); -``` - -If you would like to delete all metric data, you can invoke the `horizon:clear-metrics` Artisan command: - -```shell -php artisan horizon:clear-metrics -``` - -## Deleting Failed Jobs - -If you would like to delete a failed job, you may use the `horizon:forget` command. The `horizon:forget` command accepts the ID or UUID of the failed job as its only argument: - -```shell -php artisan horizon:forget 5 -``` - -If you would like to delete all failed jobs, you may provide the `--all` option to the `horizon:forget` command: - -```shell -php artisan horizon:forget --all -``` - -## Clearing Jobs From Queues - -If you would like to delete all jobs from your application's default queue, you may do so using the `horizon:clear` Artisan command: - -```shell -php artisan horizon:clear -``` - -You may provide the `queue` option to delete jobs from a specific queue: - -```shell -php artisan horizon:clear --queue=emails -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/http-client.md b/_archive/docs/0.3/docs/http-client.md deleted file mode 100644 index f3f34c9ad0..0000000000 --- a/_archive/docs/0.3/docs/http-client.md +++ /dev/null @@ -1,722 +0,0 @@ -# Http Client -[[toc]] - -## Introduction - -Hypervel provides an expressive, minimal API around the [Guzzle HTTP client](http://docs.guzzlephp.org/en/stable/), allowing you to quickly make outgoing HTTP requests to communicate with other web applications. Hypervel's wrapper around Guzzle is focused on its most common use cases and a wonderful developer experience. - -Before getting started, you should ensure that you have installed the Guzzle package as a dependency of your application. By default, Hypervel automatically includes this dependency. However, if you have previously removed the package, you may install it again via Composer: - -```shell:no-line-numbers -composer require guzzlehttp/guzzle -``` - -## Making Requests - -To make requests, you may use the `head`, `get`, `post`, `put`, `patch`, and `delete` methods provided by the `Http` facade. First, let's examine how to make a basic `GET` request to another URL: - - -```php -use Hypervel\Support\Facades\Http; - -$response = Http::get('http://example.com'); -``` - -The `get` method returns an instance of `Hypervel\HttpClient\Response`, which provides a variety of methods that may be used to inspect the response: - -```php -$response->body() : string; -$response->json($key = null, $default = null) : array|mixed; -$response->object() : object; -$response->collect($key = null) : Hypervel\Support\Collection; -$response->status() : int; -$response->successful() : bool; -$response->redirect(): bool; -$response->failed() : bool; -$response->clientError() : bool; -$response->header($header) : string; -$response->headers() : array; -``` - -The `Hypervel\HttpClient\Response` object also implements the PHP `ArrayAccess` interface, allowing you to access JSON response data directly on the response: - -```php -return Http::get('http://example.com/users/1')['name']; -``` - -In addition to the response methods listed above, the following methods may be used to determine if the response has a given status code: - -```php -$response->ok() : bool; // 200 OK -$response->created() : bool; // 201 Created -$response->accepted() : bool; // 202 Accepted -$response->noContent() : bool; // 204 No Content -$response->movedPermanently() : bool; // 301 Moved Permanently -$response->found() : bool; // 302 Found -$response->badRequest() : bool; // 400 Bad Request -$response->unauthorized() : bool; // 401 Unauthorized -$response->paymentRequired() : bool; // 402 Payment Required -$response->forbidden() : bool; // 403 Forbidden -$response->notFound() : bool; // 404 Not Found -$response->requestTimeout() : bool; // 408 Request Timeout -$response->conflict() : bool; // 409 Conflict -$response->unprocessableEntity() : bool; // 422 Unprocessable Entity -$response->tooManyRequests() : bool; // 429 Too Many Requests -$response->serverError() : bool; // 500 Internal Server Error -``` - -#### URI Templates - -The HTTP client also allows you to construct request URLs using the [URI template specification](https://www.rfc-editor.org/rfc/rfc6570). To define the URL parameters that can be expanded by your URI template, you may use the `withUrlParameters` method: - -```php -Http::withUrlParameters([ - 'endpoint' => 'https://hypervel.org', - 'page' => 'docs', - 'version' => '9.x', - 'topic' => 'validation', -])->get('{+endpoint}/{page}/{version}/{topic}'); -``` - -#### Dumping Requests - -If you would like to dump the outgoing request instance before it is sent and terminate the script's execution, you may add the `dd` method to the beginning of your request definition: - -```php -return Http::dd()->get('http://example.com'); -``` - -### Request Data - -Of course, it is common when making `POST`, `PUT`, and `PATCH` requests to send additional data with your request, so these methods accept an array of data as their second argument. By default, data will be sent using the `application/json` content type: - -```php -use Hypervel\Support\Facades\Http; - -$response = Http::post('http://example.com/users', [ - 'name' => 'Steve', - 'role' => 'Network Administrator', -]); -``` - -#### GET Request Query Parameters - -When making `GET` requests, you may either append a query string to the URL directly or pass an array of key / value pairs as the second argument to the `get` method: - -```php -$response = Http::get('http://example.com/users', [ - 'name' => 'Taylor', - 'page' => 1, -]); -``` - -Alternatively, the `withQueryParameters` method may be used: - -```php -Http::retry(3, 100)->withQueryParameters([ - 'name' => 'Taylor', - 'page' => 1, -])->get('http://example.com/users'); -``` - -#### Sending Form URL Encoded Requests - -If you would like to send data using the `application/x-www-form-urlencoded` content type, you should call the `asForm` method before making your request: - -```php -$response = Http::asForm()->post('http://example.com/users', [ - 'name' => 'Sara', - 'role' => 'Privacy Consultant', -]); -``` - -#### Sending a Raw Request Body - -You may use the `withBody` method if you would like to provide a raw request body when making a request. The content type may be provided via the method's second argument: - -```php -$response = Http::withBody( - base64_encode($photo), 'image/jpeg' -)->post('http://example.com/photo'); -``` - -#### Multi-Part Requests - -If you would like to send files as multi-part requests, you should call the `attach` method before making your request. This method accepts the name of the file and its contents. If needed, you may provide a third argument which will be considered the file's filename, while a fourth argument may be used to provide headers associated with the file: - -```php -$response = Http::attach( - 'attachment', file_get_contents('photo.jpg'), 'photo.jpg', ['Content-Type' => 'image/jpeg'] -)->post('http://example.com/attachments'); -``` - -Instead of passing the raw contents of a file, you may pass a stream resource: - -```php -$photo = fopen('photo.jpg', 'r'); - -$response = Http::attach( - 'attachment', $photo, 'photo.jpg' -)->post('http://example.com/attachments'); -``` - -### Headers - -Headers may be added to requests using the `withHeaders` method. This `withHeaders` method accepts an array of key / value pairs: - -```php -$response = Http::withHeaders([ - 'X-First' => 'foo', - 'X-Second' => 'bar' -])->post('http://example.com/users', [ - 'name' => 'Taylor', -]); -``` - -You may use the `accept` method to specify the content type that your application is expecting in response to your request: - -```php -$response = Http::accept('application/json')->get('http://example.com/users'); -``` - -For convenience, you may use the `acceptJson` method to quickly specify that your application expects the `application/json` content type in response to your request: - -```php -$response = Http::acceptJson()->get('http://example.com/users'); -``` - -The `withHeaders` method merges new headers into the request's existing headers. If needed, you may replace all of the headers entirely using the `replaceHeaders` method: - -```php -$response = Http::withHeaders([ - 'X-Original' => 'foo', -])->replaceHeaders([ - 'X-Replacement' => 'bar', -])->post('http://example.com/users', [ - 'name' => 'Taylor', -]); -``` - -### Authentication - -You may specify basic and digest authentication credentials using the `withBasicAuth` and `withDigestAuth` methods, respectively: - -```php -// Basic authentication... -$response = Http::withBasicAuth('albert@hypervel.org', 'secret')->post(/* ... */); - -// Digest authentication... -$response = Http::withDigestAuth('albert@hypervel.org', 'secret')->post(/* ... */); -``` - -#### Bearer Tokens - -If you would like to quickly add a bearer token to the request's `Authorization` header, you may use the `withToken` method: - -```php -$response = Http::withToken('token')->post(/* ... */); -``` - -### Timeout - -The `timeout` method may be used to specify the maximum number of seconds to wait for a response. By default, the HTTP client will timeout after 30 seconds: - -```php -$response = Http::timeout(3)->get(/* ... */); -``` - -If the given timeout is exceeded, an instance of `Hypervel\HttpClient\ConnectionException` will be thrown. - -You may specify the maximum number of seconds to wait while trying to connect to a server using the `connectTimeout` method: - -```php -$response = Http::connectTimeout(3)->get(/* ... */); -``` - -### Retries - -If you would like the HTTP client to automatically retry the request if a client or server error occurs, you may use the `retry` method. The `retry` method accepts the maximum number of times the request should be attempted and the number of milliseconds that Hypervel should wait in between attempts: - -```php -$response = Http::retry(3, 100)->post(/* ... */); -``` - -If you would like to manually calculate the number of milliseconds to sleep between attempts, you may pass a closure as the second argument to the `retry` method: - -```php -use Exception; - -$response = Http::retry(3, function (int $attempt, Exception $exception) { - return $attempt * 100; -})->post(/* ... */); -``` - -For convenience, you may also provide an array as the first argument to the `retry` method. This array will be used to determine how many milliseconds to sleep between subsequent attempts: - -```php -$response = Http::retry([100, 200])->post(/* ... */); -``` - -If needed, you may pass a third argument to the `retry` method. The third argument should be a callable that determines if the retries should actually be attempted. For example, you may wish to only retry the request if the initial request encounters an `ConnectionException`: - -```php -use Exception; -use Hypervel\HttpClient\PendingRequest; - -$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) { - return $exception instanceof ConnectionException; -})->post(/* ... */); -``` - -If a request attempt fails, you may wish to make a change to the request before a new attempt is made. You can achieve this by modifying the request argument provided to the callable you provided to the `retry` method. For example, you might want to retry the request with a new authorization token if the first attempt returned an authentication error: - -```php -use Exception; -use Hypervel\HttpClient\PendingRequest; -use Hypervel\HttpClient\RequestException; - -$response = Http::withToken($this->getToken())->retry(2, 0, function (Exception $exception, PendingRequest $request) { - if (! $exception instanceof RequestException || $exception->response->status() !== 401) { - return false; - } - - $request->withToken($this->getNewToken()); - - return true; -})->post(/* ... */); -``` - -If all of the requests fail, an instance of `Hypervel\HttpClient\RequestException` will be thrown. If you would like to disable this behavior, you may provide a `throw` argument with a value of `false`. When disabled, the last response received by the client will be returned after all retries have been attempted: - -```php -$response = Http::retry(3, 100, throw: false)->post(/* ... */); -``` - -::: warning -If all of the requests fail because of a connection issue, a `Hypervel\HttpClient\ConnectionException` will still be thrown even when the `throw` argument is set to `false`. -::: - -### Error Handling - -Unlike Guzzle's default behavior, Hypervel's HTTP client wrapper does not throw exceptions on client or server errors (`400` and `500` level responses from servers). You may determine if one of these errors was returned using the `successful`, `clientError`, or `serverError` methods: - -```php -// Determine if the status code is >= 200 and < 300... -$response->successful(); - -// Determine if the status code is >= 400... -$response->failed(); - -// Determine if the response has a 400 level status code... -$response->clientError(); - -// Determine if the response has a 500 level status code... -$response->serverError(); - -// Immediately execute the given callback if there was a client or server error... -$response->onError(callable $callback); -``` - -#### Throwing Exceptions - -If you have a response instance and would like to throw an instance of `Hypervel\HttpClient\RequestException` if the response status code indicates a client or server error, you may use the `throw` or `throwIf` methods: - -```php -use Hypervel\HttpClient\Response; - -$response = Http::post(/* ... */); - -// Throw an exception if a client or server error occurred... -$response->throw(); - -// Throw an exception if an error occurred and the given condition is true... -$response->throwIf($condition); - -// Throw an exception if an error occurred and the given closure resolves to true... -$response->throwIf(fn (Response $response) => true); - -// Throw an exception if an error occurred and the given condition is false... -$response->throwUnless($condition); - -// Throw an exception if an error occurred and the given closure resolves to false... -$response->throwUnless(fn (Response $response) => false); - -// Throw an exception if the response has a specific status code... -$response->throwIfStatus(403); - -// Throw an exception unless the response has a specific status code... -$response->throwUnlessStatus(200); - -return $response['user']['id']; -``` - -The `Hypervel\HttpClient\RequestException` instance has a public `$response` property which will allow you to inspect the returned response. - -The `throw` method returns the response instance if no error occurred, allowing you to chain other operations onto the `throw` method: - -```php -return Http::post(/* ... */)->throw()->json(); -``` - -If you would like to perform some additional logic before the exception is thrown, you may pass a closure to the `throw` method. The exception will be thrown automatically after the closure is invoked, so you do not need to re-throw the exception from within the closure: - -```php -use Hypervel\HttpClient\Response; -use Hypervel\HttpClient\RequestException; - -return Http::post(/* ... */)->throw(function (Response $response, RequestException $e) { - // ... -})->json(); -``` - -### Guzzle Middleware - -Since Hypervel's HTTP client is powered by Guzzle, you may take advantage of [Guzzle Middleware](https://docs.guzzlephp.org/en/stable/handlers-and-middleware.html) to manipulate the outgoing request or inspect the incoming response. To manipulate the outgoing request, register a Guzzle middleware via the `withRequestMiddleware` method: - -```php -use Hypervel\Support\Facades\Http; -use Psr\Http\Message\RequestInterface; - -$response = Http::withRequestMiddleware( - function (RequestInterface $request) { - return $request->withHeader('X-Example', 'Value'); - } -)->get('http://example.com'); -``` - -Likewise, you can inspect the incoming HTTP response by registering a middleware via the `withResponseMiddleware` method: - -```php -use Hypervel\Support\Facades\Http; -use Psr\Http\Message\ResponseInterface; - -$response = Http::withResponseMiddleware( - function (ResponseInterface $response) { - $header = $response->getHeader('X-Example'); - - // ... - - return $response; - } -)->get('http://example.com'); -``` - -#### Global Middleware - -Sometimes, you may want to register a middleware that applies to every outgoing request and incoming response. To accomplish this, you may use the `globalRequestMiddleware` and `globalResponseMiddleware` methods. Typically, these methods should be invoked in the `boot` method of your application's `AppServiceProvider`: - -```php -use Hypervel\Support\Facades\Http; - -Http::globalRequestMiddleware(fn ($request) => $request->withHeader( - 'User-Agent', 'Example Application/1.0' -)); - -Http::globalResponseMiddleware(fn ($response) => $response->withHeader( - 'X-Finished-At', now()->toDateTimeString() -)); -``` - -### Guzzle Options - -You may specify additional [Guzzle request options](http://docs.guzzlephp.org/en/stable/request-options.html) using the `withOptions` method. The `withOptions` method accepts an array of key / value pairs: - -```php -$response = Http::withOptions([ - 'debug' => true, -])->get('http://example.com/users'); -``` - -## Concurrent Requests - -Sometimes, you may wish to make multiple HTTP requests concurrently. In other words, you want several requests to be dispatched at the same time instead of issuing the requests sequentially. This can lead to substantial performance improvements when interacting with slow HTTP APIs. - -Unlike Laravel, http client in Hypervel doesn't provide an additional `pool` method for concurrent requests. Because all the http requests in Hypervel are coroutine, and you can request them concurrently easily with `Hypervel\Coroutine\parallel` function. - -```php -use Hypervel\Support\Facades\Http; - -use function Hypervel\Coroutine\parallel; - -$responses = parallel([ - fn () => Http::get('http://localhost/first'), - fn () => Http::get('http://localhost/second'), - fn () => Http::get('http://localhost/third'), -]); - -return $responses[0]->ok() && - $responses[1]->ok() && - $responses[2]->ok(); -``` - -As you can see, each response instance can be accessed based on the order it was added to the array. If you wish, you can name the requests using array key, which allows you to access the corresponding responses by name: - -```php -use Hypervel\Support\Facades\Http; - -$responses = parallel([ - 'first' => fn () => Http::get('http://localhost/first'), - 'second' => fn () => Http::get('http://localhost/second'), - 'third' => fn () => Http::get('http://localhost/third'), -]); - -return $responses['first']->ok(); -``` - -## Macros - -The Hypervel HTTP client allows you to define "macros", which can serve as a fluent, expressive mechanism to configure common request paths and headers when interacting with services throughout your application. To get started, you may define the macro within the `boot` method of your application's `App\Providers\AppServiceProvider` class: - -```php -use Hypervel\Support\Facades\Http; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Http::macro('github', function () { - return Http::withHeaders([ - 'X-Example' => 'example', - ])->baseUrl('https://github.com'); - }); -} -``` - -Once your macro has been configured, you may invoke it from anywhere in your application to create a pending request with the specified configuration: - -```php -$response = Http::github()->get('/'); -``` - -## Testing - -Many Hypervel services provide functionality to help you easily and expressively write tests, and Hypervel's HTTP client is no exception. The `Http` facade's `fake` method allows you to instruct the HTTP client to return stubbed / dummy responses when requests are made. - -### Faking Responses - -For example, to instruct the HTTP client to return empty, `200` status code responses for every request, you may call the `fake` method with no arguments: - -```php -use Hypervel\Support\Facades\Http; - -Http::fake(); - -$response = Http::post(/* ... */); -``` - -#### Faking Specific URLs - -Alternatively, you may pass an array to the `fake` method. The array's keys should represent URL patterns that you wish to fake and their associated responses. The `*` character may be used as a wildcard character. Any requests made to URLs that have not been faked will actually be executed. You may use the `Http` facade's `response` method to construct stub / fake responses for these endpoints: - -```php -Http::fake([ - // Stub a JSON response for GitHub endpoints... - 'github.com/*' => Http::response(['foo' => 'bar'], 200, $headers), - - // Stub a string response for Google endpoints... - 'google.com/*' => Http::response('Hello World', 200, $headers), -]); -``` - -If you would like to specify a fallback URL pattern that will stub all unmatched URLs, you may use a single `*` character: - -```php -Http::fake([ - // Stub a JSON response for GitHub endpoints... - 'github.com/*' => Http::response(['foo' => 'bar'], 200, ['Headers']), - - // Stub a string response for all other endpoints... - '*' => Http::response('Hello World', 200, ['Headers']), -]); -``` - -#### Faking Response Sequences - -Sometimes you may need to specify that a single URL should return a series of fake responses in a specific order. You may accomplish this using the `Http::sequence` method to build the responses: - -```php -Http::fake([ - // Stub a series of responses for GitHub endpoints... - 'github.com/*' => Http::sequence() - ->push('Hello World', 200) - ->push(['foo' => 'bar'], 200) - ->pushStatus(404), -]); -``` - -When all the responses in a response sequence have been consumed, any further requests will cause the response sequence to throw an exception. If you would like to specify a default response that should be returned when a sequence is empty, you may use the `whenEmpty` method: - -```php -Http::fake([ - // Stub a series of responses for GitHub endpoints... - 'github.com/*' => Http::sequence() - ->push('Hello World', 200) - ->push(['foo' => 'bar'], 200) - ->whenEmpty(Http::response()), -]); -``` - -If you would like to fake a sequence of responses but do not need to specify a specific URL pattern that should be faked, you may use the `Http::fakeSequence` method: - -```php -Http::fakeSequence() - ->push('Hello World', 200) - ->whenEmpty(Http::response()); -``` - -#### Fake Callback - -If you require more complicated logic to determine what responses to return for certain endpoints, you may pass a closure to the `fake` method. This closure will receive an instance of `Hypervel\HttpClient\Request` and should return a response instance. Within your closure, you may perform whatever logic is necessary to determine what type of response to return: - -```php -use Hypervel\HttpClient\Request; - -Http::fake(function (Request $request) { - return Http::response('Hello World', 200); -}); -``` - -### Preventing Stray Requests - -If you would like to ensure that all requests sent via the HTTP client have been faked throughout your individual test or complete test suite, you can call the `preventStrayRequests` method. After calling this method, any requests that do not have a corresponding fake response will throw an exception rather than making the actual HTTP request: - -```php -use Hypervel\Support\Facades\Http; - -Http::preventStrayRequests(); - -Http::fake([ - 'github.com/*' => Http::response('ok'), -]); - -// An "ok" response is returned... -Http::get('https://github.com/hypervel/hypervel'); - -// An exception is thrown... -Http::get('https://hypervel.org'); -``` - -### Inspecting Requests - -When faking responses, you may occasionally wish to inspect the requests the client receives in order to make sure your application is sending the correct data or headers. You may accomplish this by calling the `Http::assertSent` method after calling `Http::fake`. - -The `assertSent` method accepts a closure which will receive an `Hypervel\HttpClient\Request` instance and should return a boolean value indicating if the request matches your expectations. In order for the test to pass, at least one request must have been issued matching the given expectations: - -```php -use Hypervel\HttpClient\Request; -use Hypervel\Support\Facades\Http; - -Http::fake(); - -Http::withHeaders([ - 'X-First' => 'foo', -])->post('http://example.com/users', [ - 'name' => 'Taylor', - 'role' => 'Developer', -]); - -Http::assertSent(function (Request $request) { - return $request->hasHeader('X-First', 'foo') && - $request->url() == 'http://example.com/users' && - $request['name'] == 'Taylor' && - $request['role'] == 'Developer'; -}); -``` - -If needed, you may assert that a specific request was not sent using the `assertNotSent` method: - -```php -use Hypervel\HttpClient\Request; -use Hypervel\Support\Facades\Http; - -Http::fake(); - -Http::post('http://example.com/users', [ - 'name' => 'Taylor', - 'role' => 'Developer', -]); - -Http::assertNotSent(function (Request $request) { - return $request->url() === 'http://example.com/posts'; -}); -``` - -You may use the `assertSentCount` method to assert how many requests were "sent" during the test: - -```php -Http::fake(); - -Http::assertSentCount(5); -``` - -Or, you may use the `assertNothingSent` method to assert that no requests were sent during the test: - -```php -Http::fake(); - -Http::assertNothingSent(); -``` - -#### Recording Requests / Responses - -You may use the `recorded` method to gather all requests and their corresponding responses. The `recorded` method returns a collection of arrays that contains instances of `Hypervel\HttpClient\Request` and `Hypervel\HttpClient\Response`: - -```php -Http::fake([ - 'https://hypervel.org' => Http::response(status: 500), - 'https://hypervel.org/docs' => Http::response(), -]); - -Http::get('https://hypervel.org'); -Http::get('https://hypervel.org/docs'); - -$recorded = Http::recorded(); - -[$request, $response] = $recorded[0]; -``` - -Additionally, the `recorded` method accepts a closure which will receive an instance of `Hypervel\HttpClient\Request` and `Hypervel\HttpClient\Response` and may be used to filter request / response pairs based on your expectations: - -```php -use Hypervel\HttpClient\Request; -use Hypervel\HttpClient\Response; - -Http::fake([ - 'https://hypervel.org' => Http::response(status: 500), - 'https://hypervel.org/docs' => Http::response(), -]); - -Http::get('https://hypervel.org'); -Http::get('https://hypervel.org/docs'); - -$recorded = Http::recorded(function (Request $request, Response $response) { - return $request->url() !== 'https://hypervel.org' && - $response->successful(); -}); -``` - -## Events - -Hypervel fires three events during the process of sending HTTP requests. The `RequestSending` event is fired prior to a request being sent, while the `ResponseReceived` event is fired after a response is received for a given request. The `ConnectionFailed` event is fired if no response is received for a given request. - -The `RequestSending` and `ConnectionFailed` events both contain a public `$request` property that you may use to inspect the `Hypervel\HttpClient\Request` instance. Likewise, the `ResponseReceived` event contains a `$request` property as well as a `$response` property which may be used to inspect the `Hypervel\HttpClient\Response` instance. You may register event listeners for this event in your `App\Providers\EventServiceProvider` service provider: - -```php -/** - * The event listener mappings for the application. - * - * @var array - */ -protected $listen = [ - 'Hypervel\HttpClient\Events\RequestSending' => [ - 'App\Listeners\LogRequestSending', - ], - 'Hypervel\HttpClient\Events\ResponseReceived' => [ - 'App\Listeners\LogResponseReceived', - ], - 'Hypervel\HttpClient\Events\ConnectionFailed' => [ - 'App\Listeners\LogConnectionFailed', - ], -]; -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/http-tests.md b/_archive/docs/0.3/docs/http-tests.md deleted file mode 100644 index 60b2a89fb1..0000000000 --- a/_archive/docs/0.3/docs/http-tests.md +++ /dev/null @@ -1,713 +0,0 @@ -# HTTP Tests -[[toc]] - -## Introduction - -Hypervel provides a very fluent API for making HTTP requests to your application and examining the output. For example, take a look at the feature test defined below: - -```php -get('/'); - - $response->assertStatus(200); - - } -} -``` - -The `get` method makes a `GET` request into the application, while the `assertStatus` method asserts that the returned response should have the given HTTP status code. In addition to this simple assertion, Hypervel also contains a variety of assertions for inspecting the response headers, content, JSON structure, and more. - -### Customizing Request Headers - -You may use the `withHeaders` method to customize the request's headers before it is sent to the application. This allows you to add any custom headers you would like to the request: - -```php - withHeaders([ - 'X-Header' => 'Value', - ])->json('POST', '/user', ['name' => 'Sally']); - - $response - ->assertStatus(201) - ->assertJson([ - 'created' => true, - ]); - } - } -``` - -::: tip -The CSRF middleware is automatically disabled when running tests. -::: - -### Cookies - -You may use the `withCookie` or `withCookies` methods to set cookie values before making a request. The `withCookie` method accepts a cookie name and value as its two arguments, while the `withCookies` method accepts an array of name / value pairs: - -```php -withCookie('color', 'blue')->get('/'); - - $response = $this->withCookies([ - 'color' => 'blue', - 'name' => 'Taylor', - ])->get('/'); - } -} -``` - -### Debugging Responses - -After making a test request to your application, the `dump`, `dumpHeaders`, and `dumpSession` methods may be used to examine and debug the response contents: - -```php -get('/'); - - $response->dumpHeaders(); - - $response->dumpSession(); - - $response->dump(); - } -} -``` - -## Session / Authentication - -Hypervel provides several helpers for working with the session during HTTP testing. First, you may set the session data to a given array using the `withSession` method. This is useful for loading the session with data before issuing a request to your application: - -```php -withSession(['foo' => 'bar']) - ->get('/'); - } -} -``` - -One common use of the session is for maintaining state for the authenticated user. The `actingAs` helper method provides a simple way to authenticate a given user as the current user. For example, we may use a [model factory](/docs/database-testing#writing-factories) to generate and authenticate a user: - -```php -create(); - - $response = $this->actingAs($user) - ->withSession(['foo' => 'bar']) - ->get('/'); - } -} -``` - -You may also specify which guard should be used to authenticate the given user by passing the guard name as the second argument to the `actingAs` method: - -```php -$this->actingAs($user, 'api') -``` - -## Testing JSON APIs - -Hypervel also provides several helpers for testing JSON APIs and their responses. For example, the `json`, `getJson`, `postJson`, `putJson`, `patchJson`, `deleteJson`, and `optionsJson` methods may be used to issue JSON requests with various HTTP verbs. You may also easily pass data and headers to these methods. To get started, let's write a test to make a `POST` request to `/user` and assert that the expected data was returned: - -```php -postJson('/user', ['name' => 'Sally']); - - $response - ->assertStatus(201) - ->assertJson([ - 'created' => true, - ]); - } -} -``` - -::: tip -The `assertJson` method converts the response to an array and utilizes `PHPUnit::assertArraySubset` to verify that the given array exists within the JSON response returned by the application. So, if there are other properties in the JSON response, this test will still pass as long as the given fragment is present. -::: - -In addition, JSON response data may be accessed as array variables on the response: - -```php -$this->assertTrue($response['created']); -``` - -### Verifying An Exact JSON Match - -If you would like to verify that the given array is an **exact** match for the JSON returned by the application, you should use the `assertExactJson` method: - -```php -json('POST', '/user', ['name' => 'Sally']); - - $response - ->assertStatus(201) - ->assertExactJson([ - 'created' => true, - ]); - } -} -``` - -### Verifying JSON Paths - -If you would like to verify that the JSON response contains some given data at a specified path, you should use the `assertJsonPath` method: - -```php -json('POST', '/user', ['name' => 'Sally']); - - $response - ->assertStatus(201) - ->assertJsonPath('team.owner.name', 'foo') - } -} -``` - -## Testing File Uploads - -The `Hypervel\Http\UploadedFile` class provides a `fake` method which may be used to generate dummy files or images for testing. This, combined with the `Storage` facade's `fake` method greatly simplifies the testing of file uploads. For example, you may combine these two features to easily test an avatar upload form: - -```php -image('avatar.jpg'); - - $response = $this->json('POST', '/avatar', [ - 'avatar' => $file, - ]); - - // Assert the file was stored... - Storage::disk('avatars')->assertExists($file->hashName()); - - // Assert a file does not exist... - Storage::disk('avatars')->assertMissing('missing.jpg'); - } -} -``` - -#### Fake File Customization - -When creating files using the `fake` method, you may specify the width, height, and size of the image in order to better test your validation rules: - -```php -UploadedFile::fake()->image('avatar.jpg', $width, $height)->size(100); -``` - -In addition to creating images, you may create files of any other type using the `create` method: - -```php -UploadedFile::fake()->create('document.pdf', $sizeInKilobytes); -``` - -If needed, you may pass a `$mimeType` argument to the method to explicitly define the MIME type that should be returned by the file: - -```php -UploadedFile::fake()->create('document.pdf', $sizeInKilobytes, 'application/pdf'); -``` - -## Available Assertions - -### Response Assertions - -Hypervel provides a variety of custom assertion methods for your [PHPUnit](https://phpunit.de/) feature tests. These assertions may be accessed on the response that is returned from the `json`, `get`, `post`, `put`, and `delete` test methods: - - - -
    - -[assertCookie](#assert-cookie) -[assertCookieExpired](#assert-cookie-expired) -[assertCookieNotExpired](#assert-cookie-not-expired) -[assertCookieMissing](#assert-cookie-missing) -[assertCreated](#assert-created) -[assertDontSee](#assert-dont-see) -[assertDontSeeText](#assert-dont-see-text) -[assertExactJson](#assert-exact-json) -[assertForbidden](#assert-forbidden) -[assertHeader](#assert-header) -[assertHeaderMissing](#assert-header-missing) -[assertJson](#assert-json) -[assertJsonCount](#assert-json-count) -[assertJsonFragment](#assert-json-fragment) -[assertJsonMissing](#assert-json-missing) -[assertJsonMissingExact](#assert-json-missing-exact) -[assertJsonMissingValidationErrors](#assert-json-missing-validation-errors) -[assertJsonPath](#assert-json-path) -[assertJsonStructure](#assert-json-structure) -[assertJsonValidationErrors](#assert-json-validation-errors) -[assertLocation](#assert-location) -[assertNoContent](#assert-no-content) -[assertNotFound](#assert-not-found) -[assertOk](#assert-ok) -[assertPlainCookie](#assert-plain-cookie) -[assertRedirect](#assert-redirect) -[assertSee](#assert-see) -[assertSeeInOrder](#assert-see-in-order) -[assertSeeText](#assert-see-text) -[assertSeeTextInOrder](#assert-see-text-in-order) -[assertSessionHas](#assert-session-has) -[assertSessionHasInput](#assert-session-has-input) -[assertSessionHasAll](#assert-session-has-all) -[assertSessionHasErrors](#assert-session-has-errors) -[assertSessionHasErrorsIn](#assert-session-has-errors-in) -[assertSessionHasNoErrors](#assert-session-has-no-errors) -[assertSessionDoesntHaveErrors](#assert-session-doesnt-have-errors) -[assertSessionMissing](#assert-session-missing) -[assertStatus](#assert-status) -[assertSuccessful](#assert-successful) -[assertUnauthorized](#assert-unauthorized) - -
    - -#### assertCookie - -Assert that the response contains the given cookie: - -```php -$response->assertCookie($cookieName, $value = null); -``` - -#### assertCookieExpired - -Assert that the response contains the given cookie and it is expired: - -```php -$response->assertCookieExpired($cookieName); -``` - -#### assertCookieNotExpired - -Assert that the response contains the given cookie and it is not expired: - -```php -$response->assertCookieNotExpired($cookieName); -``` - -#### assertCookieMissing - -Assert that the response does not contains the given cookie: - -```php -$response->assertCookieMissing($cookieName); -``` - -#### assertCreated - -Assert that the response has a 201 status code: - -```php -$response->assertCreated(); -``` - -#### assertDontSee - -Assert that the given string is not contained within the response. This assertion will automatically escape the given string unless you pass a second argument of `false`: - -```php -$response->assertDontSee($value, $escaped = true); -``` - -#### assertDontSeeText - -Assert that the given string is not contained within the response text. This assertion will automatically escape the given string unless you pass a second argument of `false`: - -```php -$response->assertDontSeeText($value, $escaped = true); -``` - -#### assertExactJson - -Assert that the response contains an exact match of the given JSON data: - -```php -$response->assertExactJson(array $data); -``` - -#### assertForbidden - -Assert that the response has a forbidden (403) status code: - -```php -$response->assertForbidden(); -``` - -#### assertHeader - -Assert that the given header is present on the response: - -```php -$response->assertHeader($headerName, $value = null); -``` - -#### assertHeaderMissing - -Assert that the given header is not present on the response: - -```php -$response->assertHeaderMissing($headerName); -``` - -#### assertJson - -Assert that the response contains the given JSON data: - -```php -$response->assertJson(array $data, $strict = false); -``` - -#### assertJsonCount - -Assert that the response JSON has an array with the expected number of items at the given key: - -```php -$response->assertJsonCount($count, $key = null); -``` - -#### assertJsonFragment - -Assert that the response contains the given JSON fragment: - -```php -$response->assertJsonFragment(array $data); -``` - -#### assertJsonMissing - -Assert that the response does not contain the given JSON fragment: - -```php -$response->assertJsonMissing(array $data); -``` - -#### assertJsonMissingExact - -Assert that the response does not contain the exact JSON fragment: - -```php -$response->assertJsonMissingExact(array $data); -``` - -#### assertJsonMissingValidationErrors - -Assert that the response has no JSON validation errors for the given keys: - -```php -$response->assertJsonMissingValidationErrors($keys); -``` - -#### assertJsonPath - -Assert that the response contains the given data at the specified path: - -```php -$response->assertJsonPath($path, array $data, $strict = false); -``` - -#### assertJsonStructure - -Assert that the response has a given JSON structure: - -```php -$response->assertJsonStructure(array $structure); -``` - -#### assertJsonValidationErrors - -Assert that the response has the given JSON validation errors: - -```php -$response->assertJsonValidationErrors(array $data); -``` - -#### assertLocation - -Assert that the response has the given URI value in the `Location` header: - -```php -$response->assertLocation($uri); -``` - -#### assertNoContent - -Assert that the response has the given status code and no content. - -```php -$response->assertNoContent($status = 204); -``` - -#### assertNotFound - -Assert that the response has a not found status code: - -```php -$response->assertNotFound(); -``` - -#### assertOk - -Assert that the response has a 200 status code: - -```php -$response->assertOk(); -``` - -#### assertPlainCookie - -Assert that the response contains the given cookie (unencrypted): - -```php -$response->assertPlainCookie($cookieName, $value = null); -``` - -#### assertRedirect - -Assert that the response is a redirect to a given URI: - -```php -$response->assertRedirect($uri); -``` - -#### assertSee - -Assert that the given string is contained within the response. This assertion will automatically escape the given string unless you pass a second argument of `false`: - -```php -$response->assertSee($value, $escaped = true); -``` - -#### assertSeeInOrder - -Assert that the given strings are contained in order within the response. This assertion will automatically escape the given strings unless you pass a second argument of `false`: - -```php -$response->assertSeeInOrder(array $values, $escaped = true); -``` - -#### assertSeeText - -Assert that the given string is contained within the response text. This assertion will automatically escape the given string unless you pass a second argument of `false`: - -```php -$response->assertSeeText($value, $escaped = true); -``` - -#### assertSeeTextInOrder - -Assert that the given strings are contained in order within the response text. This assertion will automatically escape the given strings unless you pass a second argument of `false`: - -```php -$response->assertSeeTextInOrder(array $values, $escaped = true); -``` - -#### assertSessionHas - -Assert that the session contains the given piece of data: - -```php -$response->assertSessionHas($key, $value = null); -``` - -#### assertSessionHasInput - -Assert that the session has a given value in the flashed input array: - -```php -$response->assertSessionHasInput($key, $value = null); -``` - -#### assertSessionHasAll - -Assert that the session has a given list of values: - -```php -$response->assertSessionHasAll(array $data); -``` - -#### assertSessionHasErrors - -Assert that the session contains an error for the given `$keys`. If `$keys` is an associative array, assert that the session contains a specific error message (value) for each field (key): - -```php -$response->assertSessionHasErrors(array $keys, $format = null, $errorBag = ``` -'default'); -``` - -#### assertSessionHasErrorsIn - -Assert that the session contains an error for the given `$keys`, within a specific error bag. If `$keys` is an associative array, assert that the session contains a specific error message (value) for each field (key), within the error bag: - -```php -$response->assertSessionHasErrorsIn($errorBag, $keys = [], $format = null); -``` - -#### assertSessionHasNoErrors - -Assert that the session has no errors: - -```php -$response->assertSessionHasNoErrors(); -``` - -#### assertSessionDoesntHaveErrors - -Assert that the session has no errors for the given keys: - -```php -$response->assertSessionDoesntHaveErrors($keys = [], $format = null, $errorBag ``` -= 'default'); - -#### assertSessionMissing - -Assert that the session does not contain the given key: - -```php -$response->assertSessionMissing($key); -``` - -#### assertStatus - -Assert that the response has a given code: - -```php -$response->assertStatus($code); -``` - -#### assertSuccessful - -Assert that the response has a successful (>= 200 and < 300) status code: - -```php -$response->assertSuccessful(); -``` - -#### assertUnauthorized - -Assert that the response has an unauthorized (401) status code: - -```php -$response->assertUnauthorized(); -``` - -### Authentication Assertions - -Hypervel also provides a variety of authentication related assertions for your [PHPUnit](https://phpunit.de/) feature tests: - -Method | Description -------------- | ------------- -`$this->assertAuthenticated($guard = null);` | Assert that the user is authenticated. -`$this->assertGuest($guard = null);` | Assert that the user is not authenticated. -`$this->assertAuthenticatedAs($user, $guard = null);` | Assert that the given user is authenticated. -`$this->assertCredentials(array $credentials, $guard = null);` | Assert that the given credentials are valid. -`$this->assertInvalidCredentials(array $credentials, $guard = null);` | Assert that the given credentials are invalid. \ No newline at end of file diff --git a/_archive/docs/0.3/docs/installation.md b/_archive/docs/0.3/docs/installation.md deleted file mode 100644 index 4c731196a7..0000000000 --- a/_archive/docs/0.3/docs/installation.md +++ /dev/null @@ -1,129 +0,0 @@ -# Installation -[[toc]] - -## Creating a Hypervel Project - -Before creating your first Hypervel project, make sure that your local machine has [Composer](https://getcomposer.org/) installed for dependencies management, and the Swoole PHP extension must be installed as well. Typically, this can be done via PECL: - -```shell:no-line-numbers -pecl install swoole -``` - -Mac users can also install Swoole via brew: - -```shell:no-line-numbers -brew tap shivammathur/extensions -brew install shivammathur/extensions/swoole@8.3 -``` - -> Using `shivammathur/extensions` can help you solve common issues while installing swoole on Apple M1 environments. Please substitute the PHP version with the end of extension formula. - -::: info -You can also use [Box](https://hyperf.wiki/3.1/#/en/eco/box.md) as your runtime environment. -::: - -::: tip -It's recommended to set `swoole.use_shortname` to `Off` in your `php.ini` file for catching errors in coroutines if you plan to use global coroutine helpers such as `go`, `co` and `defer`. Otherwise it will throw `Uncaught RuntimeException` when errors in coroutines are not properly caught. - -See [Wrong Way to Handle Throwables](/docs/coroutine#error-handling-in-coroutines) for more information. -::: - -After you have installed PHP, Composer and Swoole extension, you may create a new Hypervel project via Composer's `create-project` command: - -```shell:no-line-numbers -composer create-project hypervel/hypervel example-app -``` - -Once the project has been created, start Hypervel's local development server using serve command: - -```shell:no-line-numbers -cd example-app - -php artisan serve -``` - -Because all the files will be kept in the memory after running, you need to restart server after you make file changes. In development, you can use hot reload command: - -```shell:no-line-numbers -php artisan watch -``` - -Once you have started the HTTP server, your application will be accessible in your web browser at [http://localhost:9501](http://localhost:9501). Next, you're ready to start taking your next steps into Hypervel. - -::: tip -You can customize your `host` and `port` and other server options in `config/server.php`. -::: - -## Initial Configuration - -All of the configuration files for the Hypervel are stored in the config directory. Each option is documented, so feel free to look through the files and get familiar with the options available to you. - -You may wish to review the `config/app.php` file and its documentation. It contains several options such as `timezone` and `locale` that you may wish to change according to your application. - -## Environment Based Configuration - -Since many of Hypervel's configuration option values may vary depending on whether your application is running on your local machine or on a production web server, many important configuration values are defined using the `.env` file that exists at the root of your application. - -Your `.env` file should not be committed to your application's source control, since each developer / server using your application could require a different environment configuration. Furthermore, this would be a security risk in the event an intruder gains access to your source control repository, since any sensitive credentials would be exposed. - -::: note -For more information about the `.env` file and environment based configuration, check out the full [configuration documentation](/docs/configuration#environment-configuration). -::: - -## Databases and Migrations - -Now that you have created your Hypervel application, you probably want to store some data in a database. By default, your application's `.env` configuration file specifies that Laravel will be interacting with a SQLite database. - -During the creation of the project, Laravel created a `database/database.sqlite` file for you, and ran the necessary migrations to create the application's database tables. - -If you prefer to use another database driver such as MySQL, you can update your `.env` configuration file to use the appropriate database. For example, if you wish to use MySQL, update your `.env` configuration file's `DB_*` variables like so: - -```shell:no-line-numbers -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=laravel_hyperf -DB_USERNAME=root -DB_PASSWORD= -``` - -If you choose to use a database other than SQLite, you will need to create the database and run your application's [database migrations](/docs/migrations): - -```shell:no-line-numbers -php artisan migrate -``` - -## Developing with Docker - -If your environment doesn't meet Hypervel's requirements or you're unfamiliar with system configuration, you can develop your Hypervel project using Docker: - -- Run Container - -In the following example the host will be mapped to the local directory `/workspace/hypervel`: - -::: note -If the `selinux-enabled` option is enabled when docker starts, access to host resources in the container will be restricted, so you should add the `--privileged -u root` option when starting the container. -::: - -```shell:no-line-numbers -docker run --name hypervel \ --v /workspace/hypervel:/data/project \ --p 9501:9501 -it \ ---privileged -u root \ ---entrypoint /bin/sh \ -hyperf/hyperf:8.3-alpine-v3.19-swoole-v6 -``` - -::: note -If docker's selinux-enabled option is on, add `--privileged -u root` to the command. -::: - -## Incompatible extensions - -Because Hypervel is based on Swoole's coroutine functionality, some extensions are incompatible in the coroutine environment. The following (including but not limited to) extensions are currently incompatible: - -- xhprof -- xdebug (It's available in PHP 8.1+ and Swoole >= 5.0.2) -- blackfire -- trace -- uopz \ No newline at end of file diff --git a/_archive/docs/0.3/docs/introduction.md b/_archive/docs/0.3/docs/introduction.md deleted file mode 100644 index db5fb89620..0000000000 --- a/_archive/docs/0.3/docs/introduction.md +++ /dev/null @@ -1,227 +0,0 @@ -## Introduction - -**Hypervel** is a Laravel-style PHP framework with native coroutine support for ultra-high performance. - -Hypervel ports many core components from Laravel while maintaining familiar usage patterns, making it instantly accessible to Laravel developers. The framework combines the elegant and expressive development experience of Laravel with the powerful performance benefits of coroutine-based programming. If you're a Laravel developer, you'll feel right at home with this framework, requiring minimal learning curve. - -This is an ideal choice for building microservices, API gateways, and high-concurrency applications where traditional PHP frameworks often encounter performance constraints. - -## Why Hypervel? - -While Laravel Octane impressively enhances your Laravel application's performance, it's crucial to understand the nature of modern web applications. In most cases, the majority of latency stems from I/O operations, such as file operations, database queries, and API requests. - -However, Laravel doesn't support coroutines - the entire framework is designed for a blocking I/O environment. Applications heavily dependent on I/O operations will still face performance bottlenecks. Consider this scenario: - -Imagine building an AI-powered chatbot where each conversation API takes 3-5 seconds to respond. With 10 workers in Laravel Octane receiving 10 concurrent requests, all workers would be blocked until these requests complete. - -For I/O-intensive scenarios, even with Laravel Octane's improvements, your application's ability to handle concurrent requests is still limited by the duration of these I/O operations. Hypervel addresses this issue by leveraging coroutines, allowing for efficient handling of concurrent I/O operations without blocking workers. This approach can significantly improve the performance and concurrency of applications with heavy I/O requirements. - -Moreover, it's unlikely that Laravel Octane will support coroutines in the near future (see [this issue](https://github.com/laravel/octane/issues/765)), given that only Swoole runtime currently supports this feature and considering backward compatibility with the framework and third-party packages. - -::: important -Even if Laravel Octane supported coroutines, these coroutines would still be limited to a single request, with workers remaining blocked until all I/O operations within that request completed. That means your Laravel application can't get better QPS results in this kind of scenario. - -See [this pull request](https://github.com/swoole/swoole-src/pull/4330) for more information. -::: - -## Laravel Octane - -Laravel is renowned for its expressive and elegant syntax, powerful package ecosystem, and thriving community. However, due to the traditional lifecycle in PHP-FPM, Laravel's performance is limited despite various optimization techniques applied to the framework. - -At LaraCon 2021, Taylor Otwell announced Laravel Octane — a first-party package maintained by the Laravel team. Octane significantly enhances Laravel application performance by running your code in a long-lived process on high-powered application servers such as Swoole, RoadRunner, and FrankenPHP. - -> For more information, see [Laravel Octane](https://laravel.com/docs/master/octane). - -## Hyperf - -Hypervel is built on the Hyperf ecosystem, similar to how Laravel relates to Symfony. Hyperf is a high-performance framework powered by Swoole and Swow, with all components natively supporting coroutines and strictly adhering to PSR standards. It allows developers to easily build high-concurrency applications with built-in support for non-blocking I/O. - -The Hyperf project maintains high activity levels on GitHub, with regular feature updates and version releases. This demonstrates strong community engagement and consistent development progress. With over 6,000 stars on GitHub and more than 350 contributors since 2019, Hyperf is a top choice for developers seeking a modern web framework to build high-performance PHP projects with asynchronous I/O support. - -> For more details, visit [Hyperf's official website](https://hyperf.io/). - -## Benchmark - -The benchmark tests cover two distinct scenarios to evaluate performance under different conditions: - -1. Simple API Test: - - A basic `hello world` API endpoint - - No middleware applied - - Tests raw response speed - -2. Simulated I/O Wait Test: - - Sleep for one second to simulate I/O wait time - - Respond with `hello world` after the delay - - Tests performance under I/O-bound conditions - -> worker numbers are configured as CPU cores by default - -Test Environment Specifications: -- Hardware: Apple M1 Pro 2021 -- CPU: 8 cores -- RAM: 16 GB - -### Simple API Test - -* Laravel Octane (8 workers) - -```text:no-line-numbers -Running 10s test @ http://127.0.0.1:8000/api - 4 threads and 100 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 15.93ms 16.86ms 155.82ms 87.02% - Req/Sec 2.07k 420.46 3.10k 66.00% - 82661 requests in 10.04s, 16.95MB read -Requests/sec: 8230.97 -Transfer/sec: 1.69MB -``` - -* Hypervel (8 workers) - -```text:no-line-numbers -Running 10s test @ http://127.0.0.1:9501/api - 4 threads and 100 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 7.66ms 17.85ms 249.92ms 90.25% - Req/Sec 24.42k 10.47k 54.37k 68.53% - 971692 requests in 10.06s, 151.98MB read -Requests/sec: 96562.80 -Transfer/sec: 15.10MB -``` - -::: chart - -```json -{ - "type": "bar", - "data": { - "labels": ["Hello World API"], - "datasets": [ - { - "label": "Laravel Octane", - "data": [8230.97], - "backgroundColor": "rgba(255, 182, 193, 0.8)", - "borderColor": "rgba(255, 182, 193, 1)", - "borderWidth": 1 - }, - { - "label": "Hypervel", - "data": [96562.80], - "backgroundColor": "rgba(173, 216, 230, 0.8)", - "borderColor": "rgba(173, 216, 230, 1)", - "borderWidth": 1 - } - ] - }, - "options": { - "responsive": true, - "scales": { - "x": { - "stacked": false - }, - "y": { - "stacked": false, - "title": { - "display": true, - "text": "QPS" - } - } - }, - "plugins": { - "title": { - "display": true, - "text": "Laravel Octane vs Hypervel" - }, - "legend": { - "position": "top" - } - } - } -} -``` -::: - -### Simulated I/O Wait Test - -* Laravel Octane (8 workers) - -```text:no-line-numbers -Running 10s test @ http://127.0.0.1:8000/api - 4 threads and 100 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 1.03s 184.92us 1.03s 87.50% - Req/Sec 1.52 1.29 5.00 54.84% - 80 requests in 10.10s, 16.80KB read - Socket errors: connect 0, read 0, write 0, timeout 72 -Requests/sec: 7.92 -Transfer/sec: 1.66KB -``` - -* Hypervel (8 workers) - -```text:no-line-numbers -Running 10s test @ http://10.10.4.12:9501/api - 16 threads and 15000 connections - Thread Stats Avg Stdev Max +/- Stdev - Latency 1.02s 64.72ms 1.87s 93.62% - Req/Sec 1.16k 1.68k 9.15k 87.59% - 109401 requests in 10.09s, 19.82MB read -Requests/sec: 10842.71 -Transfer/sec: 1.96MB -``` - -> In this case I run wrk in another machine to make sure wrk can use as much resource for keeping more connections. - -::: chart - -```json -{ - "type": "bar", - "data": { - "labels": ["1s wait I/O"], - "datasets": [ - { - "label": "Laravel Octane", - "data": [7.92], - "backgroundColor": "rgba(255, 182, 193, 0.8)", - "borderColor": "rgba(255, 182, 193, 1)", - "borderWidth": 1 - }, - { - "label": "Hypervel", - "data": [10842.71], - "backgroundColor": "rgba(173, 216, 230, 0.8)", - "borderColor": "rgba(173, 216, 230, 1)", - "borderWidth": 1 - } - ] - }, - "options": { - "responsive": true, - "scales": { - "x": { - "stacked": false - }, - "y": { - "stacked": false, - "title": { - "display": true, - "text": "QPS" - } - } - }, - "plugins": { - "title": { - "display": true, - "text": "Laravel Octane vs Hypervel" - }, - "legend": { - "position": "top" - } - } - } -} -``` -::: - -> Laravel Octane's QPS number is close to 8, but because it differs so significantly from Hypervel's numbers, it appears this way when generated in the chart. \ No newline at end of file diff --git a/_archive/docs/0.3/docs/lifecycle.md b/_archive/docs/0.3/docs/lifecycle.md deleted file mode 100644 index a1ecb7f27d..0000000000 --- a/_archive/docs/0.3/docs/lifecycle.md +++ /dev/null @@ -1,60 +0,0 @@ -# Lifecycle -[[toc]] - -## Introduction - -When using any tool in the "real world", you feel more confident if you understand how that tool works. Application development is no different. When you understand how your development tools function, you feel more comfortable and confident using them. - -The goal of this document is to give you a good, high-level overview of how Hypervel works. By getting to know the overall framework better, everything feels less "magical" and you will be more confident building your applications. If you don't understand all of the terms right away, don't lose heart! Just try to get a basic grasp of what is going on, and your knowledge will grow as you explore other sections of the documentation. - -## Lifecycle Overview - -### First Steps - -The entry point for all requests to a Hypervel application is the `artisan` file. The `artisan` file doesn't contain much code. Rather, it is a starting point for loading the rest of the framework. - -The `artisan` file loads the Composer generated autoloader definition and environment variables, and then retrieves an instance of the Hypervel application from `bootstrap/app.php`. The first action taken by Hypervel itself is to create an instance of the application / [service container](/docs/container). - -::: note -`BASE_PATH` constant is defined in `artisan`. Although we suggest fetching base path from service container, components in Hyperf framework have strong coupling to `BASE_PATH` constant. Therefore you can still find `BASE_PATH` in Hypervel for backward compatibility. -::: - -### Container Bindings - -Same as in Laravel, the application itself is a service container. When the application created, dependencies scanner collected container bindings from `config/dependencies.php`. This is how dependencies are registered in Hyperf by default. Hypervel also supports registering dependencies in service providers. - -### Console Kernel - -Next, the console kernel defines an array of `bootstrappers` that will be run before executing the command. These bootstrappers load facade aliases, register service providers, load commands and setup cronjob scheduling. Typically, these classes handle internal Hypervel configurations that you do not need to worry about. - -### Config Providers / Service Providers - -Config Providers (from Hyperf framework) are similar to Laravel's `Service Providers`. Config Providers will provide all the configuration information of the corresponding components, which will be started by Hypervel when loaded. Essentially every major feature offered by Hypervel is bootstrapped and configured by config providers. - -Hypervel also supports [service providers](/docs/providers) in a manner similar to Laravel. Service providers are responsible for bootstrapping the framework's various components and are a fundamental part of the Hypervel application lifecycle. - -Hypervel will iterate through this list of providers and instantiate each of them. After instantiating the providers, the `register` method will be called on all of the providers. Then, once all of the providers have been registered, the `boot` method will be called on each provider. This is so service providers may depend on every container binding being registered and available by the time their `boot` method is executed. - -While the framework internally uses dozens of Config Providers / Service Providers, you also have the option to create your own. If you're going to create a third-party package, we recommend using `Config Provider` or providing both `Config Provider` and `Service Provider` to ensure compatibility of Hyperf framework. - -### Routing - -Once the application has been bootstrapped and all service providers have been registered, an HTTP server will be launched and wait for incoming requests. - -The `Request` will be handed off to the router for dispatching. The router will dispatch the request to a route or controller, as well as run any route specific middleware. - -Middleware provide a convenient mechanism for filtering or examining HTTP requests entering your application. For example, Hypervel includes a middleware that verifies if the user of your application is authenticated. If the user is not authenticated, it will throw `AuthenticationException`. You can set up your middleware in a Laravel style by configuring `app/Http/Kernel.php`. To learn more about middleware, you can refer to the complete [middleware documentation](/docs/middleware). - -If the request successfully passes through all of the matched route's assigned middleware, the route or controller method will be executed. The response returned by the route or controller method will then be sent back through the route's chain of middleware. This allows middleware to perform actions both before and after your core application logic executes. - -Middleware in Hypervel can filter requests, modify responses, and even terminate the request cycle entirely if necessary. By configuring middleware in `app/Http/Kernel.php`, you can define global middleware, assign middleware to specific routes, or create middleware groups for convenient assignment. - -For more detailed information on creating, registering, and using middleware in Hypervel, including examples and best practices, please refer to the [middleware documentation](/docs/middleware). - -## Focus on Config Providers / Service Providers - -Config Providers / Service providers are truly the key to bootstrapping a Hypervel application. The application instance is created, the service providers are registered, and the request is handed to the bootstrapped application. It's really that simple! - -Having a firm grasp of how a Hypervel application is built and bootstrapped via service providers is very valuable. Your application's user-defined service providers are stored in the `app/Providers` directory. - -By default, the `AppServiceProvider` is fairly empty. This provider is a great place to add your application's own bootstrapping and service container bindings. For large applications, you may wish to create several service providers, each with more granular bootstrapping for specific services used by your application. diff --git a/_archive/docs/0.3/docs/localization.md b/_archive/docs/0.3/docs/localization.md deleted file mode 100644 index 9508e0bc68..0000000000 --- a/_archive/docs/0.3/docs/localization.md +++ /dev/null @@ -1,199 +0,0 @@ -# Localization -[[toc]] - -## Introduction - -Hypervel's localization features provide a convenient way to retrieve strings in various languages, allowing you to easily support multiple languages within your application. - -Hypervel provides two ways to manage translation strings. First, language strings may be stored in files within the application's `lang` directory. Within this directory, there may be subdirectories for each language supported by the application. This is the approach Hypervel uses to manage translation strings for built-in Hypervel features such as validation error messages: - -```plaintext:no-line-numbers -/lang - /en - messages.php - /es - messages.php -``` - -Or, translation strings may be defined within JSON files that are placed within the `lang` directory. When taking this approach, each language supported by your application would have a corresponding JSON file within this directory. This approach is recommended for applications that have a large number of translatable strings: - -```plaintext:no-line-numbers -/lang - en.json - es.json -``` - -We'll discuss each approach to managing translation strings within this documentation. - -### Configuring the Locale - -The default language for your application is stored in the `config/app.php` configuration file's `locale` configuration option. You are free to modify this value to suit the needs of your application. - -You may also configure a "fallback language", which will be used when the default language does not contain a given translation string. Like the default language, the fallback language is also configured in the `config/app.php` configuration file, and its value is typically set using the `APP_FALLBACK_LOCALE` environment variable. - -You may modify the default language for a single HTTP request at runtime using the `setLocale` method provided by the `App` facade: - -```php -use Hypervel\Support\Facades\App; - -Route::get('/greeting/{locale}', function (string $locale) { - if (! in_array($locale, ['en', 'es', 'fr'])) { - abort(400); - } - - App::setLocale($locale); - - // ... -}); -``` - -You may configure a "fallback language", which will be used when the active language does not contain a given translation string. Like the default language, the fallback language is also configured in the `config/app.php` configuration file: - -```php -'fallback_locale' => 'en', -``` - -#### Determining the Current Locale - -You may use the `currentLocale` and `isLocale` methods on the `App` facade to determine the current locale or check if the locale is a given value: - -```php -use Hypervel\Support\Facades\App; - -$locale = App::currentLocale(); - -if (App::isLocale('en')) { - // ... -} -``` - -## Defining Translation Strings - -### Using Short Keys - -Typically, translation strings are stored in files within the `lang` directory. Within this directory, there should be a subdirectory for each language supported by your application. This is the approach Hypervel uses to manage translation strings for built-in Hypervel features such as validation error messages: - -```plaintext:no-line-numbers -/lang - /en - messages.php - /es - messages.php -``` - -All language files return an array of keyed strings. For example: - -```php - 'Welcome to our application!', -]; -``` - -::: warning -For languages that differ by territory, you should name the language directories according to the ISO 15897. For example, "en_GB" should be used for British English rather than "en-gb". -::: - -### Using Translation Strings as Keys - -For applications with a large number of translatable strings, defining every string with a "short key" can become confusing when referencing the keys in your views and it is cumbersome to continually invent keys for every translation string supported by your application. - -For this reason, Laravel also provides support for defining translation strings using the "default" translation of the string as the key. Language files that use translation strings as keys are stored as JSON files in the `lang` directory. For example, if your application has a Spanish translation, you should create a `lang/es.json` file: - -```json -{ - "I love programming.": "Me encanta programar." -} -``` - -#### Key / File Conflicts - -You should not define translation string keys that conflict with other translation filenames. For example, translating `__('Action')` for the "NL" locale while a `nl/action.php` file exists but a `nl.json` file does not exist will result in the translator returning the entire contents of `nl/action.php`. - -## Retrieving Translation Strings - -You may retrieve translation strings from your language files using the `__` helper function. If you are using "short keys" to define your translation strings, you should pass the file that contains the key and the key itself to the `__` function using "dot" syntax. For example, let's retrieve the `welcome` translation string from the `lang/en/messages.php` language file: - -```php -echo __('messages.welcome'); -``` - -If the specified translation string does not exist, the `__` function will return the translation string key. So, using the example above, the `__` function would return `messages.welcome` if the translation string does not exist. - -If you are using the [Blade templating engine](/docs/blade), you may use the `{{ }}` echo syntax to display the translation string: - -```php -{{ __('messages.welcome') }} -``` - -### Replacing Parameters in Translation Strings - -If you wish, you may define placeholders in your translation strings. All placeholders are prefixed with a `:`. For example, you may define a welcome message with a placeholder name: - -```php -'welcome' => 'Welcome, :name', -``` - -To replace the placeholders when retrieving a translation string, you may pass an array of replacements as the second argument to the `__` function: - -```php -echo __('messages.welcome', ['name' => 'dayle']); -``` - -If your placeholder contains all capital letters, or only has its first letter capitalized, the translated value will be capitalized accordingly: - -```php -'welcome' => 'Welcome, :NAME', // Welcome, DAYLE -'goodbye' => 'Goodbye, :Name', // Goodbye, Dayle -``` - -### Pluralization - -Pluralization is a complex problem, as different languages have a variety of complex rules for pluralization; however, Hypervel can help you translate strings differently based on pluralization rules that you define. Using a `|` character, you may distinguish singular and plural forms of a string: - -```php -'apples' => 'There is one apple|There are many apples', -``` - -Of course, pluralization is also supported when using [translation strings as keys](#using-translation-strings-as-keys): - -```json -{ - "There is one apple|There are many apples": "Hay una manzana|Hay muchas manzanas" -} -``` - -You may even create more complex pluralization rules which specify translation strings for multiple ranges of values: - -```php -'apples' => '{0} There are none|[1,19] There are some|[20,*] There are many', -``` - -After defining a translation string that has pluralization options, you may use the `trans_choice` function to retrieve the line for a given "count". In this example, since the count is greater than one, the plural form of the translation string is returned: - -```php -echo trans_choice('messages.apples', 10); -``` - -You may also define placeholder attributes in pluralization strings. These placeholders may be replaced by passing an array as the third argument to the `trans_choice` function: - -```php -'minutes_ago' => '{1} :value minute ago|[2,*] :value minutes ago', - -echo trans_choice('time.minutes_ago', 5, ['value' => 5]); -``` - -If you would like to display the integer value that was passed to the `trans_choice` function, you may use the built-in `:count` placeholder: - -```php -'apples' => '{0} There are none|{1} There is one|[2,*] There are :count', -``` - -## Overriding Package Language Files - -Some packages may ship with their own language files. Instead of changing the package's core files to tweak these lines, you may override them by placing files in the `lang/vendor/{package}/{locale}` directory. - -So, for example, if you need to override the English translation strings in `messages.php` for a package named `skyrim/hearthfire`, you should place a language file at: `lang/vendor/hearthfire/en/messages.php`. Within this file, you should only define the translation strings you wish to override. Any translation strings you don't override will still be loaded from the package's original language files. \ No newline at end of file diff --git a/_archive/docs/0.3/docs/logging.md b/_archive/docs/0.3/docs/logging.md deleted file mode 100644 index 956f929997..0000000000 --- a/_archive/docs/0.3/docs/logging.md +++ /dev/null @@ -1,475 +0,0 @@ -# Logging -[[toc]] - -## Introduction - -To help you learn more about what's happening within your application, Hypervel provides robust logging services that allow you to log messages to files, the system error log, and even to Slack to notify your entire team. - -Hypervel's logging is based on "channels". Each channel represents a specific way of writing log information. For example, the `single` channel writes log files to a single log file, while the `slack` channel sends log messages to Slack. Log messages may be written to multiple channels based on their severity. - -Under the hood, Hypervel utilizes the [Monolog](https://github.com/Seldaek/monolog) library, which provides support for a variety of powerful log handlers. Hypervel makes it a cinch to configure these handlers, allowing you to mix and match them to customize your application's log handling. - -## Configuration - -All of the configuration options for your application's logging behavior are housed in the `config/logging.php` configuration file. This file allows you to configure your application's log channels, so be sure to review each of the available channels and their options. We'll review a few common options below. - -By default, Hypervel will use the `stack` channel when logging messages. The `stack` channel is used to aggregate multiple log channels into a single channel. For more information on building stacks, check out the [documentation below](#building-log-stacks). - -#### Configuring the Channel Name - -By default, Monolog is instantiated with a "channel name" that matches the current environment, such as `production` or `local`. To change this value, add a `name` option to your channel's configuration: - -```php -'stack' => [ - 'driver' => 'stack', - 'name' => 'channel-name', - 'channels' => ['single', 'slack'], -], -``` - -### Available Channel Drivers - -Each log channel is powered by a "driver". The driver determines how and where the log message is actually recorded. The following log channel drivers are available in every Hypervel application. An entry for most of these drivers is already present in your application's `config/logging.php` configuration file, so be sure to review this file to become familiar with its contents: - -
    - -Name | Description -------------- | ------------- -`custom` | A driver that calls a specified factory to create a channel -`daily` | A `RotatingFileHandler` based Monolog driver which rotates daily -`errorlog` | An `ErrorLogHandler` based Monolog driver -`monolog` | A Monolog factory driver that may use any supported Monolog handler -`papertrail` | A `SyslogUdpHandler` based Monolog driver -`single` | A single file or path based logger channel (`StreamHandler`) -`slack` | A `SlackWebhookHandler` based Monolog driver -`stack` | A wrapper to facilitate creating "multi-channel" channels -`syslog` | A `SyslogHandler` based Monolog driver - -
    - -::: note -Check out the documentation on [advanced channel customization](#monolog-channel-customization) to learn more about the `monolog` and `custom` drivers. -::: - -### Channel Prerequisites - -#### Configuring the Single and Daily Channels - -The `single` and `daily` channels have three optional configuration options: `bubble`, `permission`, and `locking`. - -
    - -Name | Description | Default -------------- | ------------- | ------------- -`bubble` | Indicates if messages should bubble up to other channels after being handled | `true` -`locking` | Attempt to lock the log file before writing to it | `false` -`permission` | The log file's permissions | `0644` - -
    - -Additionally, the retention policy for the `daily` channel can be configured via the `days` option: - -
    - -Name | Description | Default -------------- |-------------------------------------------------------------------| ------------- -`days` | The number of days that daily log files should be retained | `7` - -
    - -#### Configuring the Papertrail Channel - -The `papertrail` channel requires the `host` and `port` configuration options. You can obtain these values from [Papertrail](https://help.papertrailapp.com/kb/configuration/configuring-centralized-logging-from-php-apps/#send-events-from-php-app). - -#### Configuring the Slack Channel - -The `slack` channel requires a `url` configuration option. This URL should match a URL for an [incoming webhook](https://slack.com/apps/A0F7XDUAZ-incoming-webhooks) that you have configured for your Slack team. - -By default, Slack will only receive logs at the `critical` level and above; however, you can adjust this in your `config/logging.php` configuration file by modifying the `level` configuration option within your Slack log channel's configuration array. - -### Logging Deprecation Warnings - -PHP, Hypervel, and other libraries often notify their users that some of their features have been deprecated and will be removed in a future version. If you would like to log these deprecation warnings, you may specify your preferred `deprecations` log channel in your application's `config/logging.php` configuration file: - -```php -'deprecations' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), - -'channels' => [ - ... -] -``` - -Or, you may define a log channel named `deprecations`. If a log channel with this name exists, it will always be used to log deprecations: - -```php -'channels' => [ - 'deprecations' => [ - 'driver' => 'single', - 'path' => storage_path('logs/php-deprecation-warnings.log'), - ], -], -``` - -## Building Log Stacks - -As mentioned previously, the `stack` driver allows you to combine multiple channels into a single log channel for convenience. To illustrate how to use log stacks, let's take a look at an example configuration that you might see in a production application: - -```php -'channels' => [ - 'stack' => [ - 'driver' => 'stack', - 'channels' => ['syslog', 'slack'], - ], - - 'syslog' => [ - 'driver' => 'syslog', - 'level' => 'debug', - ], - - 'slack' => [ - 'driver' => 'slack', - 'url' => env('LOG_SLACK_WEBHOOK_URL'), - 'username' => 'Hypervel Log', - 'emoji' => ':boom:', - 'level' => 'critical', - ], -], -``` - -Let's dissect this configuration. First, notice our `stack` channel aggregates two other channels via its `channels` option: `syslog` and `slack`. So, when logging messages, both of these channels will have the opportunity to log the message. However, as we will see below, whether these channels actually log the message may be determined by the message's severity / "level". - -#### Log Levels - -Take note of the `level` configuration option present on the `syslog` and `slack` channel configurations in the example above. This option determines the minimum "level" a message must be in order to be logged by the channel. Monolog, which powers Hypervel's logging services, offers all of the log levels defined in the [RFC 5424 specification](https://tools.ietf.org/html/rfc5424). In descending order of severity, these log levels are: **emergency**, **alert**, **critical**, **error**, **warning**, **notice**, **info**, and **debug**. - -So, imagine we log a message using the `debug` method: - -```php -Log::debug('An informational message.'); -``` - -Given our configuration, the `syslog` channel will write the message to the system log; however, since the error message is not `critical` or above, it will not be sent to Slack. However, if we log an `emergency` message, it will be sent to both the system log and Slack since the `emergency` level is above our minimum level threshold for both channels: - -```php -Log::emergency('The system is down!'); -``` - -## Writing Log Messages - -You may write information to the logs using the `Log` [facade](/docs/facades). As previously mentioned, the logger provides the eight logging levels defined in the [RFC 5424 specification](https://tools.ietf.org/html/rfc5424): **emergency**, **alert**, **critical**, **error**, **warning**, **notice**, **info** and **debug**: - -```php -use Hypervel\Support\Facades\Log; - -Log::emergency($message); -Log::alert($message); -Log::critical($message); -Log::error($message); -Log::warning($message); -Log::notice($message); -Log::info($message); -Log::debug($message); -``` - -You may call any of these methods to log a message for the corresponding level. By default, the message will be written to the default log channel as configured by your `logging` configuration file: - -```php - $id]); - - return view('user.profile', [ - 'user' => User::findOrFail($id) - ]); - } -} -``` - -### Contextual Information - -An array of contextual data may be passed to the log methods. This contextual data will be formatted and displayed with the log message: - -```php -use Hypervel\Support\Facades\Log; - -Log::info('User {id} failed to login.', ['id' => $user->id]); -``` - -Occasionally, you may wish to specify some contextual information that should be included with all subsequent log entries in a particular channel. For example, you may wish to log a request ID that is associated with each incoming request to your application. To accomplish this, you may call the `Log` facade's `withContext` method: - -```php - $requestId - ]); - - $response = $next($request); - - $response = $response->withHeader('Request-Id', $requestId); - - return $response; - } -} -``` - -If you would like to share contextual information across _all_ logging channels, you may invoke the `Log::shareContext()` method. This method will provide the contextual information to all created channels and any channels that are created subsequently: - -```php - $requestId - ]); - - // ... - } -} -``` - -::: note -If you need to share log context while processing queued jobs, you may utilize [job middleware](/docs/queues#job-middleware). -::: - -### Writing to Specific Channels - -Sometimes you may wish to log a message to a channel other than your application's default channel. You may use the `channel` method on the `Log` facade to retrieve and log to any channel defined in your configuration file: - -```php -use Hypervel\Support\Facades\Log; - -Log::channel('slack')->info('Something happened!'); -``` - -If you would like to create an on-demand logging stack consisting of multiple channels, you may use the `stack` method: - -```php -Log::stack(['single', 'slack'])->info('Something happened!'); -``` - -#### On-Demand Channels - -It is also possible to create an on-demand channel by providing the configuration at runtime without that configuration being present in your application's `logging` configuration file. To accomplish this, you may pass a configuration array to the `Log` facade's `build` method: - -```php -use Hypervel\Support\Facades\Log; - -Log::build([ - 'driver' => 'single', - 'path' => storage_path('logs/custom.log'), -])->info('Something happened!'); -``` - -You may also wish to include an on-demand channel in an on-demand logging stack. This can be achieved by including your on-demand channel instance in the array passed to the `stack` method: - -```php -use Hypervel\Support\Facades\Log; - -$channel = Log::build([ - 'driver' => 'single', - 'path' => storage_path('logs/custom.log'), -]); - -Log::stack(['slack', $channel])->info('Something happened!'); -``` - -## Monolog Channel Customization - -### Customizing Monolog for Channels - -Sometimes you may need complete control over how Monolog is configured for an existing channel. For example, you may want to configure a custom Monolog `FormatterInterface` implementation for Hypervel's built-in `single` channel. - -To get started, define a `tap` array on the channel's configuration. The `tap` array should contain a list of classes that should have an opportunity to customize (or "tap" into) the Monolog instance after it is created. There is no conventional location where these classes should be placed, so you are free to create a directory within your application to contain these classes: - -```php -'single' => [ - 'driver' => 'single', - 'tap' => [App\Logging\CustomizeFormatter::class], - 'path' => storage_path('logs/hypervel.log'), - 'level' => 'debug', -], -``` - -Once you have configured the `tap` option on your channel, you're ready to define the class that will customize your Monolog instance. This class only needs a single method: `__invoke`, which receives an `Hypervel\Log\Logger` instance. The `Hypervel\Log\Logger` instance proxies all method calls to the underlying Monolog instance: - -```php -getHandlers() as $handler) { - $handler->setFormatter(new LineFormatter( - '[%datetime%] %channel%.%level_name%: %message% %context% %extra%' - )); - } - } -} -``` - -::: note -All of your "tap" classes are resolved by the [service container](/docs/container), so any constructor dependencies they require will automatically be injected. -::: - -### Creating Monolog Handler Channels - -Monolog has a variety of [available handlers](https://github.com/Seldaek/monolog/tree/main/src/Monolog/Handler) and Hypervel does not include a built-in channel for each one. In some cases, you may wish to create a custom channel that is merely an instance of a specific Monolog handler that does not have a corresponding Hypervel log driver. These channels can be easily created using the `monolog` driver. - -When using the `monolog` driver, the `handler` configuration option is used to specify which handler will be instantiated. Optionally, any constructor parameters the handler needs may be specified using the `with` configuration option: - -```php -'logentries' => [ - 'driver' => 'monolog', - 'handler' => Monolog\Handler\SyslogUdpHandler::class, - 'with' => [ - 'host' => 'my.logentries.internal.datahubhost.company.com', - 'port' => '10000', - ], -], -``` - -#### Monolog Formatters - -When using the `monolog` driver, the Monolog `LineFormatter` will be used as the default formatter. However, you may customize the type of formatter passed to the handler using the `formatter` and `formatter_with` configuration options: - -```php -'browser' => [ - 'driver' => 'monolog', - 'handler' => Monolog\Handler\BrowserConsoleHandler::class, - 'formatter' => Monolog\Formatter\HtmlFormatter::class, - 'formatter_with' => [ - 'dateFormat' => 'Y-m-d', - ], -], -``` - -If you are using a Monolog handler that is capable of providing its own formatter, you may set the value of the `formatter` configuration option to `default`: - -```php -'newrelic' => [ - 'driver' => 'monolog', - 'handler' => Monolog\Handler\NewRelicHandler::class, - 'formatter' => 'default', -], -``` - - #### Monolog Processors - - Monolog can also process messages before logging them. You can create your own processors or use the [existing processors offered by Monolog](https://github.com/Seldaek/monolog/tree/main/src/Monolog/Processor). - - If you would like to customize the processors for a `monolog` driver, add a `processors` configuration value to your channel's configuration: - -```php -'memory' => [ - 'driver' => 'monolog', - 'handler' => Monolog\Handler\StreamHandler::class, - 'with' => [ - 'stream' => 'php://stderr', - ], - 'processors' => [ - // Simple syntax... - Monolog\Processor\MemoryUsageProcessor::class, - - // With options... - [ - 'processor' => Monolog\Processor\PsrLogMessageProcessor::class, - 'with' => ['removeUsedContextFields' => true], - ], - ], -], -``` - -### Creating Custom Channels via Factories - -If you would like to define an entirely custom channel in which you have full control over Monolog's instantiation and configuration, you may specify a `custom` driver type in your `config/logging.php` configuration file. Your configuration should include a `via` option that contains the name of the factory class which will be invoked to create the Monolog instance: - -```php -'channels' => [ - 'example-custom-channel' => [ - 'driver' => 'custom', - 'via' => App\Logging\CreateCustomLogger::class, - ], -], -``` - -Once you have configured the `custom` driver channel, you're ready to define the class that will create your Monolog instance. This class only needs a single `__invoke` method which should return the Monolog logger instance. The method will receive the channels configuration array as its only argument: - -```php - env('MAIL_MAILER', 'mailgun'), -``` - -Second, add the following configuration array to your array of `mailers`: - -```php -'mailgun' => [ - 'transport' => 'mailgun', - // 'client' => [ - // 'timeout' => 5, - // ], -], -``` - -After configuring your application's default mailer, add the following options to your `config/services.php` configuration file: - -```php -'mailgun' => [ - 'domain' => env('MAILGUN_DOMAIN'), - 'secret' => env('MAILGUN_SECRET'), - 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), - 'scheme' => 'https', -], -``` - -If you are not using the United States [Mailgun region](https://documentation.mailgun.com/en/latest/api-intro.html#mailgun-regions), you may define your region's endpoint in the `services` configuration file: - -```php -'mailgun' => [ - 'domain' => env('MAILGUN_DOMAIN'), - 'secret' => env('MAILGUN_SECRET'), - 'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'), - 'scheme' => 'https', -], -``` - -#### Postmark Driver - -To use the [Postmark](https://postmarkapp.com/) driver, install Symfony's Postmark Mailer transport via Composer: - -```shell:no-line-numbers -composer require symfony/postmark-mailer symfony/http-client -``` - -Next, set the `default` option in your application's `config/mail.php` configuration file to `postmark`. After configuring your application's default mailer, ensure that your `config/services.php` configuration file contains the following options: - -```php -'postmark' => [ - 'token' => env('POSTMARK_TOKEN'), -], -``` - -If you would like to specify the Postmark message stream that should be used by a given mailer, you may add the `message_stream_id` configuration option to the mailer's configuration array. This configuration array can be found in your application's `config/mail.php` configuration file: - -```php -'postmark' => [ - 'transport' => 'postmark', - 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), - // 'client' => [ - // 'timeout' => 5, - // ], -], -``` - -This way you are also able to set up multiple Postmark mailers with different message streams. - -#### Resend Driver - -To use the [Resend](https://resend.com/) driver, install Resend's PHP SDK via Composer: - -```shell:no-line-numbers -composer require resend/resend-php -``` - -Next, set the `default` option in your application's `config/mail.php` configuration file to `resend`. After configuring your application's default mailer, ensure that your `config/services.php` configuration file contains the following options: - -```php -'resend' => [ - 'key' => env('RESEND_KEY'), -], -``` - -#### SES Driver - -To use the Amazon SES driver you must first install the Amazon AWS SDK for PHP. You may install this library via the Composer package manager: - -```shell:no-line-numbers -composer require aws/aws-sdk-php -``` - -Next, set the `default` option in your `config/mail.php` configuration file to `ses` and verify that your `config/services.php` configuration file contains the following options: - -```php -'ses' => [ - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), -], -``` - -To utilize AWS [temporary credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html) via a session token, you may add a `token` key to your application's SES configuration: - -```php -'ses' => [ - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), - 'token' => env('AWS_SESSION_TOKEN'), -], -``` - -To interact with SES's [subscription management features](https://docs.aws.amazon.com/ses/latest/dg/sending-email-subscription-management.html), you may return the `X-Ses-List-Management-Options` header in the array returned by the [`headers`](#headers) method of a mail message: - -```php -/** - * Get the message headers. - */ -public function headers(): Headers -{ - return new Headers( - text: [ - 'X-Ses-List-Management-Options' => 'contactListName=MyContactList;topicName=MyTopic', - ], - ); -} -``` - -If you would like to define [additional options](https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sesv2-2019-09-27.html#sendemail) that Hypervel should pass to the AWS SDK's `SendEmail` method when sending an email, you may define an `options` array within your `ses` configuration: - -```php -'ses' => [ - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), - 'options' => [ - 'ConfigurationSetName' => 'MyConfigurationSet', - 'EmailTags' => [ - ['Name' => 'foo', 'Value' => 'bar'], - ], - ], -], -``` - -#### MailerSend Driver - -[MailerSend](https://www.mailersend.com/), a transactional email and SMS service, maintains their own API based mail driver for Hypervel. The package containing the driver may be installed via the Composer package manager: - -```shell:no-line-numbers -composer require mailersend/laravel-driver -``` - -Once the package is installed, add the `MAILERSEND_API_KEY` environment variable to your application's `.env` file. In addition, the `MAIL_MAILER` environment variable should be defined as `mailersend`: - -```ini -MAIL_MAILER=mailersend -MAIL_FROM_ADDRESS=app@yourdomain.com -MAIL_FROM_NAME="App Name" - -MAILERSEND_API_KEY=your-api-key -``` - -Finally, add MailerSend to the `mailers` array in your application's `config/mail.php` configuration file: - -```php -'mailersend' => [ - 'transport' => 'mailersend', -], -``` - -To learn more about MailerSend, including how to use hosted templates, consult the [MailerSend driver documentation](https://github.com/mailersend/mailersend-laravel-driver#usage). - -### Failover Configuration - -Sometimes, an external service you have configured to send your application's mail may be down. In these cases, it can be useful to define one or more backup mail delivery configurations that will be used in case your primary delivery driver is down. - -To accomplish this, you should define a mailer within your application's `mail` configuration file that uses the `failover` transport. The configuration array for your application's `failover` mailer should contain an array of `mailers` that reference the order in which configured mailers should be chosen for delivery: - -```php -'mailers' => [ - 'failover' => [ - 'transport' => 'failover', - 'mailers' => [ - 'postmark', - 'mailgun', - 'sendmail', - ], - ], - - // ... -], -``` - -Once your failover mailer has been defined, you should set this mailer as the default mailer used by your application by specifying its name as the value of the `default` configuration key within your application's `mail` configuration file: - -```php -'default' => env('MAIL_MAILER', 'failover'), -``` - -### Round Robin Configuration - -The `roundrobin` transport allows you to distribute your mailing workload across multiple mailers. To get started, define a mailer within your application's `mail` configuration file that uses the `roundrobin` transport. The configuration array for your application's `roundrobin` mailer should contain an array of `mailers` that reference which configured mailers should be used for delivery: - -```php -'mailers' => [ - 'roundrobin' => [ - 'transport' => 'roundrobin', - 'mailers' => [ - 'ses', - 'postmark', - ], - ], - - // ... -], -``` - -Once your round robin mailer has been defined, you should set this mailer as the default mailer used by your application by specifying its name as the value of the `default` configuration key within your application's `mail` configuration file: - -```php -'default' => env('MAIL_MAILER', 'roundrobin'), -``` - -The round robin transport selects a random mailer from the list of configured mailers and then switches to the next available mailer for each subsequent email. In contrast to `failover` transport, which helps to achieve *[high availability](https://en.wikipedia.org/wiki/High_availability)*, the `roundrobin` transport provides *[load balancing](https://en.wikipedia.org/wiki/Load_balancing_(computing))*. - -## Generating Mailables - -When building Hypervel applications, each type of email sent by your application is represented as a "mailable" class. These classes are stored in the `app/Mail` directory. Don't worry if you don't see this directory in your application, since it will be generated for you when you create your first mailable class using the `make:mail` Artisan command: - -```shell:no-line-numbers -php artisan make:mail OrderShipped -``` - -## Writing Mailables - -Once you have generated a mailable class, open it up so we can explore its contents. Mailable class configuration is done in several methods, including the `envelope`, `content`, and `attachments` methods. - -The `envelope` method returns an `Hypervel\Mail\Mailables\Envelope` object that defines the subject and, sometimes, the recipients of the message. The `content` method returns an `Hypervel\Mail\Mailables\Content` object that defines the [Blade template](/docs/blade) that will be used to generate the message content. - -### Configuring the Sender - -#### Using the Envelope - -First, let's explore configuring the sender of the email. Or, in other words, who the email is going to be "from". There are two ways to configure the sender. First, you may specify the "from" address on your message's envelope: - -```php -use Hypervel\Mail\Mailables\Address; -use Hypervel\Mail\Mailables\Envelope; - -/** - * Get the message envelope. - */ -public function envelope(): Envelope -{ - return new Envelope( - from: new Address('jeffrey@example.com', 'Jeffrey Way'), - subject: 'Order Shipped', - ); -} -``` - -If you would like, you may also specify a `replyTo` address: - -```php -return new Envelope( - from: new Address('jeffrey@example.com', 'Jeffrey Way'), - replyTo: [ - new Address('taylor@example.com', 'Taylor Otwell'), - ], - subject: 'Order Shipped', -); -``` - -#### Using a Global `from` Address - -However, if your application uses the same "from" address for all of its emails, it can become cumbersome to add it to each mailable class you generate. Instead, you may specify a global "from" address in your `config/mail.php` configuration file. This address will be used if no other "from" address is specified within the mailable class: - -```php -'from' => [ - 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name' => env('MAIL_FROM_NAME', 'Example'), -], -``` - -In addition, you may define a global "reply_to" address within your `config/mail.php` configuration file: - -```php -'reply_to' => ['address' => 'example@example.com', 'name' => 'App Name'], -``` - -### Configuring the View - -Within a mailable class's `content` method, you may define the `view`, or which template should be used when rendering the email's contents. Since each email typically uses a [Blade template](/docs/blade) to render its contents, you have the full power and convenience of the Blade templating engine when building your email's HTML: - -```php -/** - * Get the message content definition. - */ -public function content(): Content -{ - return new Content( - view: 'mail.orders.shipped', - ); -} -``` - -::: note -You may wish to create a `resources/views/emails` directory to house all of your email templates; however, you are free to place them wherever you wish within your `resources/views` directory. -::: - -#### Plain Text Emails - -If you would like to define a plain-text version of your email, you may specify the plain-text template when creating the message's `Content` definition. Like the `view` parameter, the `text` parameter should be a template name which will be used to render the contents of the email. You are free to define both an HTML and plain-text version of your message: - -```php -/** - * Get the message content definition. - */ -public function content(): Content -{ - return new Content( - view: 'mail.orders.shipped', - text: 'mail.orders.shipped-text' - ); -} -``` - -For clarity, the `html` parameter may be used as an alias of the `view` parameter: - -```php -return new Content( - html: 'mail.orders.shipped', - text: 'mail.orders.shipped-text' -); -``` - -### View Data - -#### Via Public Properties - -Typically, you will want to pass some data to your view that you can utilize when rendering the email's HTML. There are two ways you may make data available to your view. First, any public property defined on your mailable class will automatically be made available to the view. So, for example, you may pass data into your mailable class's constructor and set that data to public properties defined on the class: - -```php - - Price: {{ $order->price }} - -``` - -#### Via the `with` Parameter: - -If you would like to customize the format of your email's data before it is sent to the template, you may manually pass your data to the view via the `Content` definition's `with` parameter. Typically, you will still pass data via the mailable class's constructor; however, you should set this data to `protected` or `private` properties so the data is not automatically made available to the template: - -```php - $this->order->name, - 'orderPrice' => $this->order->price, - ], - ); - } -} -``` - -Once the data has been passed to the `with` method, it will automatically be available in your view, so you may access it like you would access any other data in your Blade templates: - -```html -
    - Price: {{ $orderPrice }} -
    -``` - -### Attachments - -To add attachments to an email, you will add attachments to the array returned by the message's `attachments` method. First, you may add an attachment by providing a file path to the `fromPath` method provided by the `Attachment` class: - -```php -use Hypervel\Mail\Mailables\Attachment; - -/** - * Get the attachments for the message. - * - * @return array - */ -public function attachments(): array -{ - return [ - Attachment::fromPath('/path/to/file'), - ]; -} -``` - -When attaching files to a message, you may also specify the display name and / or MIME type for the attachment using the `as` and `withMime` methods: - -```php -/** - * Get the attachments for the message. - * - * @return array - */ -public function attachments(): array -{ - return [ - Attachment::fromPath('/path/to/file') - ->as('name.pdf') - ->withMime('application/pdf'), - ]; -} -``` - -#### Attaching Files From Disk - -If you have stored a file on one of your [filesystem disks](/docs/filesystem), you may attach it to the email using the `fromStorage` attachment method: - -```php -/** - * Get the attachments for the message. - * - * @return array - */ -public function attachments(): array -{ - return [ - Attachment::fromStorage('/path/to/file'), - ]; -} -``` - -Of course, you may also specify the attachment's name and MIME type: - -```php -/** - * Get the attachments for the message. - * - * @return array - */ -public function attachments(): array -{ - return [ - Attachment::fromStorage('/path/to/file') - ->as('name.pdf') - ->withMime('application/pdf'), - ]; -} -``` - -The `fromStorageDisk` method may be used if you need to specify a storage disk other than your default disk: - -```php -/** - * Get the attachments for the message. - * - * @return array - */ -public function attachments(): array -{ - return [ - Attachment::fromStorageDisk('s3', '/path/to/file') - ->as('name.pdf') - ->withMime('application/pdf'), - ]; -} -``` - -#### Raw Data Attachments - -The `fromData` attachment method may be used to attach a raw string of bytes as an attachment. For example, you might use this method if you have generated a PDF in memory and want to attach it to the email without writing it to disk. The `fromData` method accepts a closure which resolves the raw data bytes as well as the name that the attachment should be assigned: - -```php -/** - * Get the attachments for the message. - * - * @return array - */ -public function attachments(): array -{ - return [ - Attachment::fromData(fn () => $this->pdf, 'Report.pdf') - ->withMime('application/pdf'), - ]; -} -``` - -### Inline Attachments - -Embedding inline images into your emails is typically cumbersome; however, Hypervel provides a convenient way to attach images to your emails. To embed an inline image, use the `embed` method on the `$message` variable within your email template. Hypervel automatically makes the `$message` variable available to all of your email templates, so you don't need to worry about passing it in manually: - -```html - - Here is an image: - - - -``` - -::: warning -The `$message` variable is not available in plain-text message templates since plain-text messages do not utilize inline attachments. -::: - -#### Embedding Raw Data Attachments - -If you already have a raw image data string you wish to embed into an email template, you may call the `embedData` method on the `$message` variable. When calling the `embedData` method, you will need to provide a filename that should be assigned to the embedded image: - -```html - - Here is an image from raw data: - - - -``` - -### Attachable Objects - -While attaching files to messages via simple string paths is often sufficient, in many cases the attachable entities within your application are represented by classes. For example, if your application is attaching a photo to a message, your application may also have a `Photo` model that represents that photo. When that is the case, wouldn't it be convenient to simply pass the `Photo` model to the `attach` method? Attachable objects allow you to do just that. - -To get started, implement the `Hypervel\Mail\Contracts\Attachable` interface on the object that will be attachable to messages. This interface dictates that your class defines a `toMailAttachment` method that returns an `Hypervel\Mail\Attachment` instance: - -```php - - */ -public function attachments(): array -{ - return [$this->photo]; -} -``` - -Of course, attachment data may be stored on a remote file storage service such as Amazon S3. So, Hypervel also allows you to generate attachment instances from data that is stored on one of your application's [filesystem disks](/docs/filesystem): - -```php -// Create an attachment from a file on your default disk... -return Attachment::fromStorage($this->path); - -// Create an attachment from a file on a specific disk... -return Attachment::fromStorageDisk('backblaze', $this->path); -``` - -In addition, you may create attachment instances via data that you have in memory. To accomplish this, provide a closure to the `fromData` method. The closure should return the raw data that represents the attachment: - -```php -return Attachment::fromData(fn () => $this->content, 'Photo Name'); -``` - -Hypervel also provides additional methods that you may use to customize your attachments. For example, you may use the `as` and `withMime` methods to customize the file's name and MIME type: - -```php -return Attachment::fromPath('/path/to/file') - ->as('Photo Name') - ->withMime('image/jpeg'); -``` - -### Headers - -Sometimes you may need to attach additional headers to the outgoing message. For instance, you may need to set a custom `Message-Id` or other arbitrary text headers. - -To accomplish this, define a `headers` method on your mailable. The `headers` method should return an `Hypervel\Mail\Mailables\Headers` instance. This class accepts `messageId`, `references`, and `text` parameters. Of course, you may provide only the parameters you need for your particular message: - -```php -use Hypervel\Mail\Mailables\Headers; - -/** - * Get the message headers. - */ -public function headers(): Headers -{ - return new Headers( - messageId: 'custom-message-id@example.com', - references: ['previous-message@example.com'], - text: [ - 'X-Custom-Header' => 'Custom Value', - ], - ); -} -``` - -### Tags and Metadata - -Some third-party email providers such as Mailgun and Postmark support message "tags" and "metadata", which may be used to group and track emails sent by your application. You may add tags and metadata to an email message via your `Envelope` definition: - -```php -use Hypervel\Mail\Mailables\Envelope; - -/** - * Get the message envelope. - * - * @return \Hypervel\Mail\Mailables\Envelope - */ -public function envelope(): Envelope -{ - return new Envelope( - subject: 'Order Shipped', - tags: ['shipment'], - metadata: [ - 'order_id' => $this->order->id, - ], - ); -} -``` - -If your application is using the Mailgun driver, you may consult Mailgun's documentation for more information on [tags](https://documentation.mailgun.com/docs/mailgun/user-manual/tracking-messages/#tagging) and [metadata](https://documentation.mailgun.com/docs/mailgun/user-manual/tracking-messages/#attaching-data-to-messages). Likewise, the Postmark documentation may also be consulted for more information on their support for [tags](https://postmarkapp.com/blog/tags-support-for-smtp) and [metadata](https://postmarkapp.com/support/article/1125-custom-metadata-faq). - -If your application is using Amazon SES to send emails, you should use the `metadata` method to attach [SES "tags"](https://docs.aws.amazon.com/ses/latest/APIReference/API_MessageTag.html) to the message. - -### Customizing the Symfony Message - -Hypervel's mail capabilities are powered by Symfony Mailer. Hypervel allows you to register custom callbacks that will be invoked with the Symfony Message instance before sending the message. This gives you an opportunity to deeply customize the message before it is sent. To accomplish this, define a `using` parameter on your `Envelope` definition: - -```php -use Hypervel\Mail\Mailables\Envelope; -use Symfony\Component\Mime\Email; - -/** - * Get the message envelope. - */ -public function envelope(): Envelope -{ - return new Envelope( - subject: 'Order Shipped', - using: [ - function (Email $message) { - // ... - }, - ] - ); -} -``` - -## Markdown Mailables - -Markdown mailable messages allow you to take advantage of the pre-built templates and components of [mail notifications](/docs/notifications#mail-notifications) in your mailables. Since the messages are written in Markdown, Hypervel is able to render beautiful, responsive HTML templates for the messages while also automatically generating a plain-text counterpart. - -### Generating Markdown Mailables - -To generate a mailable with a corresponding Markdown template, you may use the `--markdown` option of the `make:mail` Artisan command: - -```shell:no-line-numbers -php artisan make:mail OrderShipped --markdown=mail.orders.shipped -``` - -When configuring the mailable `Content` definition within its `content` method, use the `markdown` parameter instead of the `view` parameter: - -```php -use Hypervel\Mail\Mailables\Content; - -/** - * Get the message content definition. - */ -public function content(): Content -{ - return new Content( - markdown: 'mail.orders.shipped', - with: [ - 'url' => $this->orderUrl, - ], - ); -} -``` - -> `make:mail` is not supported yet. - -### Writing Markdown Messages - -Markdown mailables use a combination of Blade components and Markdown syntax which allow you to easily construct mail messages while leveraging Hypervel's pre-built email UI components: - -```html - -# Order Shipped - -Your order has been shipped! - - -View Order - - -Thanks,
    -{{ config('app.name') }} -
    -``` - -::: note -Do not use excess indentation when writing Markdown emails. Per Markdown standards, Markdown parsers will render indented content as code blocks. -::: - -#### Button Component - -The button component renders a centered button link. The component accepts two arguments, a `url` and an optional `color`. Supported colors are `primary`, `success`, and `error`. You may add as many button components to a message as you wish: - -```html - -View Order - -``` - -#### Panel Component - -The panel component renders the given block of text in a panel that has a slightly different background color than the rest of the message. This allows you to draw attention to a given block of text: - -```html - -This is the panel content. - -``` - -#### Table Component - -The table component allows you to transform a Markdown table into an HTML table. The component accepts the Markdown table as its content. Table column alignment is supported using the default Markdown table alignment syntax: - -```html - -| Hypervel| Table | Example | -| ------------- | :-----------: | ------------: | -| Col 2 is | Centered | $10 | -| Col 3 is | Right-Aligned | $20 | - -``` - -### Customizing the Components - -You may export all of the Markdown mail components to your own application for customization. To export the components, use the `vendor:publish` Artisan command to publish mail package: - -```shell:no-line-numbers -php artisan vendor:publish hypervel/mail -``` - -This command will publish the Markdown mail components to the `resources/views/vendor/mail` directory. The `mail` directory will contain an `html` and a `text` directory, each containing their respective representations of every available component. You are free to customize these components however you like. - -#### Customizing the CSS - -After exporting the components, the `resources/views/vendor/mail/html/themes` directory will contain a `default.css` file. You may customize the CSS in this file and your styles will automatically be converted to inline CSS styles within the HTML representations of your Markdown mail messages. - -If you would like to build an entirely new theme for Hypervel's Markdown components, you may place a CSS file within the `html/themes` directory. After naming and saving your CSS file, update the `theme` option of your application's `config/mail.php` configuration file to match the name of your new theme. - -To customize the theme for an individual mailable, you may set the `$theme` property of the mailable class to the name of the theme that should be used when sending that mailable. - -## Sending Mail - -To send a message, use the `to` method on the `Mail` [facade](/docs/facades). The `to` method accepts an email address, a user instance, or a collection of users. If you pass an object or collection of objects, the mailer will automatically use their `email` and `name` properties when determining the email's recipients, so make sure these attributes are available on your objects. Once you have specified your recipients, you may pass an instance of your mailable class to the `send` method: - -```php -order_id); - - // Ship the order... - - Mail::to($request->user())->send(new OrderShipped($order)); - - return redirect('/orders'); - } -} -``` - -You are not limited to just specifying the "to" recipients when sending a message. You are free to set "to", "cc", and "bcc" recipients by chaining their respective methods together: - -```php -Mail::to($request->user()) - ->cc($moreUsers) - ->bcc($evenMoreUsers) - ->send(new OrderShipped($order)); -``` - -#### Looping Over Recipients - -Occasionally, you may need to send a mailable to a list of recipients by iterating over an array of recipients / email addresses. However, since the `to` method appends email addresses to the mailable's list of recipients, each iteration through the loop will send another email to every previous recipient. Therefore, you should always re-create the mailable instance for each recipient: - -```php -foreach (['taylor@example.com', 'dries@example.com'] as $recipient) { - Mail::to($recipient)->send(new OrderShipped($order)); -} -``` - -#### Sending Mail via a Specific Mailer - -By default, Hypervel will send email using the mailer configured as the `default` mailer in your application's `mail` configuration file. However, you may use the `mailer` method to send a message using a specific mailer configuration: - -```php -Mail::mailer('postmark') - ->to($request->user()) - ->send(new OrderShipped($order)); -``` - -### Queueing Mail - -#### Queueing a Mail Message - -Since sending email messages can negatively impact the response time of your application, many developers choose to queue email messages for background sending. Hypervel makes this easy using its built-in [unified queue API](/docs/queues). To queue a mail message, use the `queue` method on the `Mail` facade after specifying the message's recipients: - -```php -Mail::to($request->user()) - ->cc($moreUsers) - ->bcc($evenMoreUsers) - ->queue(new OrderShipped($order)); -``` - -This method will automatically take care of pushing a job onto the queue so the message is sent in the background. You will need to [configure your queues](/docs/queues) before using this feature. - -#### Delayed Message Queueing - -If you wish to delay the delivery of a queued email message, you may use the `later` method. As its first argument, the `later` method accepts a `DateTime` instance indicating when the message should be sent: - -```php -Mail::to($request->user()) - ->cc($moreUsers) - ->bcc($evenMoreUsers) - ->later(now()->addMinutes(10), new OrderShipped($order)); -``` - -#### Pushing to Specific Queues - -Since all mailable classes generated using the `make:mail` command make use of the `Hypervel\Bus\Queueable` trait, you may call the `onQueue` and `onConnection` methods on any mailable class instance, allowing you to specify the connection and queue name for the message: - -```php -$message = (new OrderShipped($order)) - ->onConnection('sqs') - ->onQueue('emails'); - -Mail::to($request->user()) - ->cc($moreUsers) - ->bcc($evenMoreUsers) - ->queue($message); -``` - -#### Queueing by Default - -If you have mailable classes that you want to always be queued, you may implement the `ShouldQueue` contract on the class. Now, even if you call the `send` method when mailing, the mailable will still be queued since it implements the contract: - -```php -use Hypervel\Queue\Contracts\ShouldQueue; - -class OrderShipped extends Mailable implements ShouldQueue -{ - // ... -} -``` - -#### Queued Mailables and Database Transactions - -When queued mailables are dispatched within database transactions, they may be processed by the queue before the database transaction has committed. When this happens, any updates you have made to models or database records during the database transaction may not yet be reflected in the database. In addition, any models or database records created within the transaction may not exist in the database. If your mailable depends on these models, unexpected errors can occur when the job that sends the queued mailable is processed. - -If your queue connection's `after_commit` configuration option is set to `false`, you may still indicate that a particular queued mailable should be dispatched after all open database transactions have been committed by calling the `afterCommit` method when sending the mail message: - -```php -Mail::to($request->user())->send( - (new OrderShipped($order))->afterCommit() -); -``` - -Alternatively, you may call the `afterCommit` method from your mailable's constructor: - -```php -afterCommit(); - } -} -``` - -::: note -To learn more about working around these issues, please review the documentation regarding [queued jobs and database transactions](/docs/queues#jobs-and-database-transactions). -::: - -## Rendering Mailables - -Sometimes you may wish to capture the HTML content of a mailable without sending it. To accomplish this, you may call the `render` method of the mailable. This method will return the evaluated HTML content of the mailable as a string: - -```php -use App\Mail\InvoicePaid; -use App\Models\Invoice; - -$invoice = Invoice::find(1); - -return (new InvoicePaid($invoice))->render(); -``` - -### Previewing Mailables in the Browser - -When designing a mailable's template, it is convenient to quickly preview the rendered mailable in your browser like a typical Blade template. For this reason, Hypervel allows you to return any mailable directly from a route closure or controller. When a mailable is returned, it will be rendered and displayed in the browser, allowing you to quickly preview its design without needing to send it to an actual email address: - -```php -Route::get('/mailable', function () { - $invoice = App\Models\Invoice::find(1); - - return new App\Mail\InvoicePaid($invoice); -}); -``` - -## Localizing Mailables - -Hypervel allows you to send mailables in a locale other than the request's current locale, and will even remember this locale if the mail is queued. - -To accomplish this, the `Mail` facade offers a `locale` method to set the desired language. The application will change into this locale when the mailable's template is being evaluated and then revert back to the previous locale when evaluation is complete: - -```php -Mail::to($request->user())->locale('es')->send( - new OrderShipped($order) -); -``` - -### User Preferred Locales - -Sometimes, applications store each user's preferred locale. By implementing the `HasLocalePreference` contract on one or more of your models, you may instruct Hypervel to use this stored locale when sending mail: - -```php -use Hypervel\Translation\Contracts\HasLocalePreference; - -class User extends Model implements HasLocalePreference -{ - /** - * Get the user's preferred locale. - */ - public function preferredLocale(): string - { - return $this->locale; - } -} -``` - -Once you have implemented the interface, Hypervel will automatically use the preferred locale when sending mailables and notifications to the model. Therefore, there is no need to call the `locale` method when using this interface: - -```php -Mail::to($request->user())->send(new OrderShipped($order)); -``` - -## Testing - -### Testing Mailable Content - -Hypervel provides a variety of methods for inspecting your mailable's structure. In addition, Hypervel provides several convenient methods for testing that your mailable contains the content that you expect. These methods are: `assertSeeInHtml`, `assertDontSeeInHtml`, `assertSeeInOrderInHtml`, `assertSeeInText`, `assertDontSeeInText`, `assertSeeInOrderInText`, `assertHasAttachment`, `assertHasAttachedData`, `assertHasAttachmentFromStorage`, and `assertHasAttachmentFromStorageDisk`. - -As you might expect, the "HTML" assertions assert that the HTML version of your mailable contains a given string, while the "text" assertions assert that the plain-text version of your mailable contains a given string: - -```php -use App\Mail\InvoicePaid; -use App\Models\User; - -public function testMailableContent(): void -{ - $user = User::factory()->create(); - - $mailable = new InvoicePaid($user); - - $mailable->assertFrom('jeffrey@example.com'); - $mailable->assertTo('taylor@example.com'); - $mailable->assertHasCc('abigail@example.com'); - $mailable->assertHasBcc('victoria@example.com'); - $mailable->assertHasReplyTo('tyler@example.com'); - $mailable->assertHasSubject('Invoice Paid'); - $mailable->assertHasTag('example-tag'); - $mailable->assertHasMetadata('key', 'value'); - - $mailable->assertSeeInHtml($user->email); - $mailable->assertSeeInHtml('Invoice Paid'); - $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']); - - $mailable->assertSeeInText($user->email); - $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']); - - $mailable->assertHasAttachment('/path/to/file'); - $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file')); - $mailable->assertHasAttachedData($pdfData, 'name.pdf', ['mime' => 'application/pdf']); - $mailable->assertHasAttachmentFromStorage('/path/to/file', 'name.pdf', ['mime' => 'application/pdf']); - $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', 'name.pdf', ['mime' => 'application/pdf']); -} -``` - -### Testing Mailable Sending - -We suggest testing the content of your mailables separately from your tests that assert that a given mailable was "sent" to a specific user. Typically, the content of mailables is not relevant to the code you are testing, and it is sufficient to simply assert that Hypervel was instructed to send a given mailable. - -You may use the `Mail` facade's `fake` method to prevent mail from being sent. After calling the `Mail` facade's `fake` method, you may then assert that mailables were instructed to be sent to users and even inspect the data the mailables received: - -```php -order->id === $order->id; -}); -``` - -When calling the `Mail` facade's assertion methods, the mailable instance accepted by the provided closure exposes helpful methods for examining the mailable: - -```php -Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($user) { - return $mail->hasTo($user->email) && - $mail->hasCc('...') && - $mail->hasBcc('...') && - $mail->hasReplyTo('...') && - $mail->hasFrom('...') && - $mail->hasSubject('...'); -}); -``` - -The mailable instance also includes several helpful methods for examining the attachments on a mailable: - -```php -use Hypervel\Mail\Mailables\Attachment; - -Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) { - return $mail->hasAttachment( - Attachment::fromPath('/path/to/file') - ->as('name.pdf') - ->withMime('application/pdf') - ); -}); - -Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) { - return $mail->hasAttachment( - Attachment::fromStorageDisk('s3', '/path/to/file') - ); -}); - -Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($pdfData) { - return $mail->hasAttachment( - Attachment::fromData(fn () => $pdfData, 'name.pdf') - ); -}); -``` - -You may have noticed that there are two methods for asserting that mail was not sent: `assertNotSent` and `assertNotQueued`. Sometimes you may wish to assert that no mail was sent **or** queued. To accomplish this, you may use the `assertNothingOutgoing` and `assertNotOutgoing` methods: - -```php -Mail::assertNothingOutgoing(); - -Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) { - return $mail->order->id === $order->id; -}); -``` - -## Mail and Local Development - -When developing an application that sends email, you probably don't want to actually send emails to live email addresses. Hypervel provides several ways to "disable" the actual sending of emails during local development. - -#### Log Driver - -Instead of sending your emails, the `log` mail driver will write all email messages to your log files for inspection. Typically, this driver would only be used during local development. For more information on configuring your application per environment, check out the [configuration documentation](/docs/configuration#environment-configuration). - -#### HELO / Mailtrap / Mailpit - -Alternatively, you may use a service like [HELO](https://usehelo.com) or [Mailtrap](https://mailtrap.io) and the `smtp` driver to send your email messages to a "dummy" mailbox where you may view them in a true email client. This approach has the benefit of allowing you to actually inspect the final emails in Mailtrap's message viewer. - -#### Using a Global `to` Address - -Finally, you may specify a global "to" address by invoking the `alwaysTo` method offered by the `Mail` facade. Typically, this method should be called from the `boot` method of one of your application's service providers: - -```php -use Hypervel\Support\Facades\Mail; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - if ($this->app->environment('local')) { - Mail::alwaysTo('taylor@example.com'); - } -} -``` - -## Events - -Hypervel dispatches two events while sending mail messages. The `MessageSending` event is dispatched prior to a message being sent, while the `MessageSent` event is dispatched after a message has been sent. Remember, these events are dispatched when the mail is being *sent*, not when it is queued. You may create [event listeners](/docs/events) for these events within your application: - -```php -use Hypervel\Mail\Events\MessageSending; -// use Hypervel\Mail\Events\MessageSent; - -class LogMessage -{ - /** - * Handle the given event. - */ - public function handle(MessageSending $event): void - { - // ... - } -} -``` - -## Custom Transports - -Hypervel includes a variety of mail transports; however, you may wish to write your own transports to deliver email via other services that Hypervel does not support out of the box. To get started, define a class that extends the `Symfony\Component\Mailer\Transport\AbstractTransport` class. Then, implement the `doSend` and `__toString()` methods on your transport: - -```php -use MailchimpTransactional\ApiClient; -use Symfony\Component\Mailer\SentMessage; -use Symfony\Component\Mailer\Transport\AbstractTransport; -use Symfony\Component\Mime\Address; -use Symfony\Component\Mime\MessageConverter; - -class MailchimpTransport extends AbstractTransport -{ - /** - * Create a new Mailchimp transport instance. - */ - public function __construct( - protected ApiClient $client, - ) { - parent::__construct(); - } - - /** - * {@inheritDoc} - */ - protected function doSend(SentMessage $message): void - { - $email = MessageConverter::toEmail($message->getOriginalMessage()); - - $this->client->messages->send(['message' => [ - 'from_email' => $email->getFrom(), - 'to' => collect($email->getTo())->map(function (Address $email) { - return ['email' => $email->getAddress(), 'type' => 'to']; - })->all(), - 'subject' => $email->getSubject(), - 'text' => $email->getTextBody(), - ]]); - } - - /** - * Get the string representation of the transport. - */ - public function __toString(): string - { - return 'mailchimp'; - } -} -``` - -Once you've defined your custom transport, you may register it via the `extend` method provided by the `Mail` facade. Typically, this should be done within the `boot` method of your application's `AppServiceProvider` service provider. A `$config` argument will be passed to the closure provided to the `extend` method. This argument will contain the configuration array defined for the mailer in the application's `config/mail.php` configuration file: - -```php -use App\Mail\MailchimpTransport; -use Hypervel\Support\Facades\Mail; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Mail::extend('mailchimp', function (array $config = []) { - return new MailchimpTransport(/* ... */); - }); -} -``` - -Once your custom transport has been defined and registered, you may create a mailer definition within your application's `config/mail.php` configuration file that utilizes the new transport: - -```php -'mailchimp' => [ - 'transport' => 'mailchimp', - // ... -], -``` - -### Additional Symfony Transports - -Hypervel includes support for some existing Symfony maintained mail transports like Mailgun and Postmark. However, you may wish to extend Hypervel with support for additional Symfony maintained transports. You can do so by requiring the necessary Symfony mailer via Composer and registering the transport with Hypervel. For example, you may install and register the "Brevo" (formerly "Sendinblue") Symfony mailer: - -```shell:no-line-numbers -composer require symfony/brevo-mailer symfony/http-client -``` - -Once the Brevo mailer package has been installed, you may add an entry for your Brevo API credentials to your application's `services` configuration file: - -```php -'brevo' => [ - 'key' => 'your-api-key', -], -``` - -Next, you may use the `Mail` facade's `extend` method to register the transport with Hypervel. Typically, this should be done within the `boot` method of a service provider: - -```php -use Hypervel\Support\Facades\Mail; -use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; -use Symfony\Component\Mailer\Transport\Dsn; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Mail::extend('brevo', function () { - return (new BrevoTransportFactory)->create( - new Dsn( - 'brevo+api', - 'default', - config('services.brevo.key') - ) - ); - }); -} -``` - -Once your transport has been registered, you may create a mailer definition within your application's config/mail.php configuration file that utilizes the new transport: - -```php -'brevo' => [ - 'transport' => 'brevo', - // ... -], -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/middleware.md b/_archive/docs/0.3/docs/middleware.md deleted file mode 100644 index b02da31094..0000000000 --- a/_archive/docs/0.3/docs/middleware.md +++ /dev/null @@ -1,366 +0,0 @@ -# Middleware -[[toc]] - -## Introduction - -Middleware provide a convenient mechanism for inspecting and filtering HTTP requests entering your application. For example, Hypervel includes a middleware that verifies the user of your application is authenticated. If the user is not authenticated, the middleware will throw `Hypervel\Auth\AuthenticationException`. However, if the user is authenticated, the middleware will allow the request to proceed further into the application. - -Additional middleware can be written to perform a variety of tasks besides authentication. For example, a logging middleware might log all incoming requests to your application. You can place your middleware in the `app/Http/Middleware` directory. - -## Middleware Support - -Hypervel supports [PSR-15](https://www.php-fig.org/psr/psr-15/) based middleware by default. This standard provides a common interface for HTTP server-side middleware, ensuring interoperability between different frameworks and libraries. - -However, middleware in Laravel doesn't follow this standard. Laravel uses its own middleware implementation, which has a different structure and method signatures compared to PSR-15 middleware. - -To ensure backward compatibility with Hyperf and support Laravel-style middleware, Hypervel allows you to use both types of middleware together. This means you can: - -1. Use PSR-15 compliant middleware for maximum interoperability with other PHP libraries and frameworks. -2. Continue using Laravel-style middleware if you're migrating an existing Laravel application or prefer the Laravel middleware syntax. - -This flexibility allows developers to choose the middleware style that best suits their needs or gradually migrate from Laravel-style to PSR-15 middleware as needed. When defining middleware in your application, you can mix and match these two types without conflict. Hypervel will handle the appropriate execution of each middleware type internally. - -::: tip -For compatibility with middleware in Hyperf, the `request` you get in the middleware is a `Psr\Http\Message\ServerRequestInterface` object, which only implements basic PSR-7 methods. If you want to use `Hypervel\Http\Request` within the middleware, you can get it by fetching `Hypervel\Http\Request` from the container, using `Hypervel\Support\Facades\Request` facade or calling `request()` global helper function. - -`Hypervel\Http\Request` implements `Psr\Http\Message\ServerRequestInterface` and provides many other useful functions like in Laravel. For more details, you can see: [requests](/docs/requests). -::: - -## Defining Hypervel Middleware - -```php -withHeader('Access-Control-Allow-Origin', '*') - ->withHeader('Access-Control-Allow-Credentials', 'true') - // Headers can be rewritten according to actual conditions. - ->withHeader('Access-Control-Allow-Headers', 'DNT,Keep-Alive,User-Agent,Cache-Control,Content-Type,Authorization'); - - Context::set(ResponseInterface::class, $response); - - if ($request->getMethod() == 'OPTIONS') { - return $response; - } - - return $handler->handle($request); - } -} -``` - -`Request` and `Response` objects in Hypervel follow the [PSR-7](https://www.php-fig.org/psr/psr-7/) standard and are therefore `immutable`. This immutability has important implications for how you work with these objects: - -When you call methods like `$response->with***()`, you're not modifying the original `$response` object. Instead, you're creating a new object through cloning. For example: - -```php -$response = $response->withHeader('X-Header', 'Value'); -``` - -In this case, `$response` is now a new object, not a modification of the original one. However, Hyperf provides a way to override these objects using context. This allows you to maintain the PSR-7 compliance while still providing a way to modify the request and response objects when necessary. - -```php -use Hypervel\Context\RequestContext; -use Hypervel\Context\ResponseContext; - -// $request and $response are the modified objects -$request = RequestContext::set($request); -$response = ResponseContext::set($response); -``` - -::: tip -When working with middleware or other parts of your application where you need to modify the request or response, make sure to return the new object rather than expecting the original to be modified in place. -::: - -## Defining Laravel Middleware - -You can also define a Laravel-style middleware like this: - -```php -getQueryParams['token'] ?? null) !== 'my-secret-token') { - throw new AuthenticationException; - } - - // But can get Laravel-like request with more features like: - if ($this->request->input('token') !== 'my-secret-token') { - throw new AuthenticationException; - } - - return $next($request); - } -} -``` - -As you can see, if the given `token` does not match our secret token, the middleware will throw `AuthenticationException`; otherwise, the request will be passed further into the application. To pass the request deeper into the application (allowing the middleware to "pass"), you should call the `$next` callback with the `$request`. - -It's best to envision middleware as a series of "layers" HTTP requests must pass through before they hit your application. Each layer can examine the request and even reject it entirely. - -::: note -All middleware are resolved via the [service container](/docs/container), so you may type-hint any dependencies you need within a middleware's constructor. -::: - -::: important -`Request` in Laravel-style middleware also follow PSR-7 and it's `immutable` as well. -::: - -#### Middleware and Responses - -Of course, a middleware can perform tasks before or after passing the request deeper into the application. For example, the following middleware would perform some task **before** the request is handled by the application: - -```php - Authenticate::class]); -``` - -You may assign multiple middleware to the route by passing an array of middleware names to the `middleware` option: - -```php -Route::get('/', function () { - // ... -}, ['middleware' => [First::class, Second::class]); -``` - -For convenience, you may assign aliases to middleware in your application's `app/Http/Kernel.php` file. By default, the `$middlewareAliases` property of this class contains entries for the middleware included with Hypervel. You may add your own middleware to this list and assign it an alias of your choosing: - -```php -// Within App\Http\Kernel class... - -protected array $middlewareAliases = [ - 'throttle' => \Hypervel\Router\Middleware\ThrottleRequests::class, - 'bindings' => \Hypervel\Router\Middleware\SubstituteBindings::class, -]; -``` - -Once the middleware alias has been defined in the HTTP kernel, you may use the alias when assigning middleware to routes: - -```php -Route::get('/profile', function () { - // ... -}, ['middleware' => 'auth']); -``` - -### Excluding Middleware - -When assigning middleware to a group of routes, you may occasionally need to prevent the middleware from being applied to an individual route within the group. You may accomplish this using the `without_middleware` option: - -```php -use App\Http\Middleware\EnsureTokenIsValid; - -Route::get('/profile', function () { - // ... -}, ['without_middleware' => EnsureTokenIsValid::class]); -``` - -You may also exclude a given set of middleware from an entire [group](/docs/middleware#middleware-groups) of route definitions: - -```php -Route::group('/', function () { - Route::get('/profile', function () { - // ... - }, ['without_middleware' => 'auth']); -}, ['middleware' => 'auth']); -``` - -### Middleware Groups - -Sometimes you may want to group several middleware under a single key to make them easier to assign to routes. You may accomplish this using the `$middlewareGroups` property of your HTTP kernel. - -Hypervel includes predefined `web` and `api` middleware groups that contain common middleware you may want to apply to your web and API routes. Remember, these middleware groups are automatically applied by your application's `App\Providers\RouteServiceProvider` service provider to routes within your corresponding `web` and `api` route files: - -```php -/** - * The application's route middleware groups. - * - * @var array - */ -protected array $middlewareGroups = [ - 'web' => [ - // \Hypervel\Router\Middleware\SubstituteBindings::class, - // \Hypervel\Cookie\Middleware\AddQueuedCookiesToResponse::class, - // \Hypervel\Session\Middleware\StartSession::class, - // \Hypervel\View\Middleware\ShareErrorsFromSession::class, - // \App\Http\Middleware\VerifyCsrfToken::class, - ], - - 'api' => [ - // 'throttle:60,1,api', - // \Hypervel\Router\Middleware\SubstituteBindings::class, - ], -]; -``` - -::: note -Middleware in `middlewareGroups` are disabled in Hypervel by default. You can enable them according to your needs. -::: - -Middleware groups may be assigned to routes and controller actions using the same syntax as individual middleware. Again, middleware groups make it more convenient to assign many middleware to a route at once: - -```php -Route::get('/', function () { - // ... -}, ['middleware' => 'web']); - -Route::group('/', function () { - // ... -}, ['middleware' => 'auth']); -``` - -::: note -Out of the box, the `web` and `api` middleware groups are automatically applied to your application's corresponding `routes/web.php` and `routes/api.php` files by the `App\Providers\RouteServiceProvider`. -::: - -### Sorting Middleware - -Rarely, you may need your middleware to execute in a specific order but not have control over their order when they are assigned to the route. In this case, you may specify your middleware priority using the `$middlewarePriority` property of your `app/Http/Kernel.php` file. This property may not exist in your HTTP kernel by default. If it does not exist, you may copy its default definition below: - -```php -/** - * The priority-sorted list of middleware. - * - * This forces non-global middleware to always be in the given order. - * - * @var string[] - */ -protected array $middlewarePriority = [ - // \Hypervel\Router\Middleware\ThrottleRequests::class, - // \Hypervel\Router\Middleware\SubstituteBindings::class, - // \Hypervel\Session\Middleware\StartSession::class, - // \Hypervel\View\Middleware\ShareErrorsFromSession::class, - // \App\Http\Middleware\VerifyCsrfToken::class, -]; -``` - -## Middleware Parameters - -Middleware can also receive additional parameters. For example, if your application needs to verify that the authenticated user has a given "role" before performing a given action, you could create an `EnsureUserHasRole` middleware that receives a role name as an additional argument. - -Additional middleware parameters will be passed to the middleware after the `$next` argument: - -```php -user()->hasRole($role)) { - // throw exception... - } - - return $next($request); - } -} -``` - -Middleware parameters may be specified when defining the route by separating the middleware name and parameters with a `:`: - -```php -Route::put('/post/{id}', function (string $id) { - // ... -}, ['middleware' => 'role:editor']); -``` diff --git a/_archive/docs/0.3/docs/migrations.md b/_archive/docs/0.3/docs/migrations.md deleted file mode 100644 index ac97ba7141..0000000000 --- a/_archive/docs/0.3/docs/migrations.md +++ /dev/null @@ -1,1353 +0,0 @@ -# Database: Migrations -[[toc]] - -## Introduction - -Migrations are like version control for your database, allowing your team to define and share the application's database schema definition. If you have ever had to tell a teammate to manually add a column to their local database schema after pulling in your changes from source control, you've faced the problem that database migrations solve. - -The Hypervel `Schema` [facade](/docs/facades) provides database agnostic support for creating and manipulating tables across all of Hypervel's supported database systems. Typically, migrations will use this facade to create and modify database tables and columns. - -## Generating Migrations - -You may use the `make:migration` [Artisan command](/docs/artisan) to generate a database migration. The new migration will be placed in your `database/migrations` directory. Each migration filename contains a timestamp that allows Hypervel to determine the order of the migrations: - -```shell:no-line-numbers -php artisan make:migration create_flights_table -``` - -Hypervel will use the name of the migration to attempt to guess the name of the table and whether or not the migration will be creating a new table. If Hypervel is able to determine the table name from the migration name, Hypervel will pre-fill the generated migration file with the specified table. Otherwise, you may simply specify the table in the migration file manually. - -If you would like to specify a custom path for the generated migration, you may use the `--path` option when executing the `make:migration` command. The given path should be relative to your application's base path. - -## Migration Structure - -A migration class contains two methods: `up` and `down`. The `up` method is used to add new tables, columns, or indexes to your database, while the `down` method should reverse the operations performed by the `up` method. - -Within both of these methods, you may use the Hypervel schema builder to expressively create and modify tables. To learn about all of the methods available on the `Schema` builder, [check out its documentation](#creating-tables). For example, the following migration creates a `flights` table: - -```php -id(); - $table->string('name'); - $table->string('airline'); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::drop('flights'); - } -}; -``` - -#### Setting the Migration Connection - -If your migration will be interacting with a database connection other than your application's default database connection, you should set the `$connection` property of your migration: - -```php -/** - * The database connection that should be used by the migration. - */ -protected ?string $connection = 'mysql'; - -/** - * Run the migrations. - */ -public function up(): void -{ - // ... -} -``` - -## Running Migrations - -To run all of your outstanding migrations, execute the `migrate` Artisan command: - -```shell:no-line-numbers -php artisan migrate -``` - -If you would like to see which migrations have run thus far, you may use the `migrate:status` Artisan command: - -```shell:no-line-numbers -php artisan migrate:status -``` - -If you would like to see the SQL statements that will be executed by the migrations without actually running them, you may provide the `--pretend` flag to the `migrate` command: - -```shell:no-line-numbers -php artisan migrate --pretend -``` - -#### Forcing Migrations to Run in Production - -Some migration operations are destructive, which means they may cause you to lose data. In order to protect you from running these commands against your production database, you will be prompted for confirmation before the commands are executed. To force the commands to run without a prompt, use the `--force` flag: - -```shell:no-line-numbers -php artisan migrate --force -``` - -### Rolling Back Migrations - -To roll back the latest migration operation, you may use the `rollback` Artisan command. This command rolls back the last "batch" of migrations, which may include multiple migration files: - -```shell:no-line-numbers -php artisan migrate:rollback -``` - -You may roll back a limited number of migrations by providing the `step` option to the `rollback` command. For example, the following command will roll back the last five migrations: - -```shell:no-line-numbers -php artisan migrate:rollback --step=5 -``` - -You may roll back a specific "batch" of migrations by providing the `batch` option to the `rollback` command, where the `batch` option corresponds to a batch value within your application's `migrations` database table. For example, the following command will roll back all migrations in batch three: - -```shell:no-line-numbers -php artisan migrate:rollback --batch=3 -``` - -If you would like to see the SQL statements that will be executed by the migrations without actually running them, you may provide the `--pretend` flag to the `migrate:rollback` command: - -```shell:no-line-numbers -php artisan migrate:rollback --pretend -``` - -The `migrate:reset` command will roll back all of your application's migrations: - -```shell:no-line-numbers -php artisan migrate:reset -``` - -#### Roll Back and Migrate Using a Single Command - -The `migrate:refresh` command will roll back all of your migrations and then execute the `migrate` command. This command effectively re-creates your entire database: - -```shell:no-line-numbers -php artisan migrate:refresh - -# Refresh the database and run all database seeds... -php artisan migrate:refresh --seed -``` - -You may roll back and re-migrate a limited number of migrations by providing the `step` option to the `refresh` command. For example, the following command will roll back and re-migrate the last five migrations: - -```shell:no-line-numbers -php artisan migrate:refresh --step=5 -``` - -#### Drop All Tables and Migrate - -The `migrate:fresh` command will drop all tables from the database and then execute the `migrate` command: - -```shell:no-line-numbers -php artisan migrate:fresh - -php artisan migrate:fresh --seed -``` - -By default, the `migrate:fresh` command only drops tables from the default database connection. However, you may use the `--database` option to specify the database connection that should be migrated. The database connection name should correspond to a connection defined in your application's `database` [configuration file](/docs/configuration): - -```shell:no-line-numbers -php artisan migrate:fresh --database=admin -``` - -::: warning -The `migrate:fresh` command will drop all database tables regardless of their prefix. This command should be used with caution when developing on a database that is shared with other applications. -::: - -## Tables - -### Creating Tables - -To create a new database table, use the `create` method on the `Schema` facade. The `create` method accepts two arguments: the first is the name of the table, while the second is a closure which receives a `Blueprint` object that may be used to define the new table: - -```php -use Hyperf\Database\Schema\Blueprint; -use Hypervel\Support\Facades\Schema; - -Schema::create('users', function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->string('email'); - $table->timestamps(); -}); -``` - -When creating the table, you may use any of the schema builder's [column methods](#creating-columns) to define the table's columns. - - -#### Determining Table / Column Existence - -You may determine the existence of a table, column, or index using the `hasTable`, `hasColumn`, and `hasIndex` methods: - -```php -if (Schema::hasTable('users')) { - // The "users" table exists... -} - -if (Schema::hasColumn('users', 'email')) { - // The "users" table exists and has an "email" column... -} - -if (Schema::hasIndex('users', ['email'], 'unique')) { - // The "users" table exists and has a unique index on the "email" column... -} -``` - -#### Database Connection and Table Options - -If you want to perform a schema operation on a database connection that is not your application's default connection, use the `connection` method: - -```php -Schema::connection('sqlite')->create('users', function (Blueprint $table) { - $table->id(); -}); -``` - -In addition, a few other properties and methods may be used to define other aspects of the table's creation. The `engine` property may be used to specify the table's storage engine when using MariaDB or MySQL: - -```php -Schema::create('users', function (Blueprint $table) { - $table->engine('InnoDB'); - - // ... -}); -``` - -The `charset` and `collation` properties may be used to specify the character set and collation for the created table when using MariaDB or MySQL: - -```php -Schema::create('users', function (Blueprint $table) { - $table->charset('utf8mb4'); - $table->collation('utf8mb4_unicode_ci'); - - // ... -}); -``` - -The `temporary` method may be used to indicate that the table should be "temporary". Temporary tables are only visible to the current connection's database session and are dropped automatically when the connection is closed: - -```php -Schema::create('calculations', function (Blueprint $table) { - $table->temporary(); - - // ... -}); -``` - -If you would like to add a "comment" to a database table, you may invoke the `comment` method on the table instance. Table comments are currently only supported by MariaDB, MySQL, and PostgreSQL: - -```php -Schema::create('calculations', function (Blueprint $table) { - $table->comment('Business calculations'); - - // ... -}); -``` - -### Updating Tables - -The `table` method on the `Schema` facade may be used to update existing tables. Like the `create` method, the `table` method accepts two arguments: the name of the table and a closure that receives a `Blueprint` instance you may use to add columns or indexes to the table: - -```php -use Hyperf\Database\Schema\Blueprint; -use Hypervel\Support\Facades\Schema; - -Schema::table('users', function (Blueprint $table) { - $table->integer('votes'); -}); -``` - -integer('votes'); -}); -``` - -### Available Column Types - -The schema builder blueprint offers a variety of methods that correspond to the different types of columns you can add to your database tables. Each of the available methods are listed in the table below: - - - -#### Boolean Types - -
    - -[boolean](#column-method-boolean) - -
    - -#### String & Text Types - -
    - -[char](#column-method-char) -[longText](#column-method-longText) -[mediumText](#column-method-mediumText) -[string](#column-method-string) -[text](#column-method-text) -[tinyText](#column-method-tinyText) - -
    - -#### Numeric Types - -
    - -[bigIncrements](#column-method-bigIncrements) -[bigInteger](#column-method-bigInteger) -[decimal](#column-method-decimal) -[double](#column-method-double) -[float](#column-method-float) -[id](#column-method-id) -[increments](#column-method-increments) -[integer](#column-method-integer) -[mediumIncrements](#column-method-mediumIncrements) -[mediumInteger](#column-method-mediumInteger) -[smallIncrements](#column-method-smallIncrements) -[smallInteger](#column-method-smallInteger) -[tinyIncrements](#column-method-tinyIncrements) -[tinyInteger](#column-method-tinyInteger) -[unsignedBigInteger](#column-method-unsignedBigInteger) -[unsignedInteger](#column-method-unsignedInteger) -[unsignedMediumInteger](#column-method-unsignedMediumInteger) -[unsignedSmallInteger](#column-method-unsignedSmallInteger) -[unsignedTinyInteger](#column-method-unsignedTinyInteger) - -
    - -#### Date & Time Types - -
    - -[dateTime](#column-method-dateTime) -[dateTimeTz](#column-method-dateTimeTz) -[date](#column-method-date) -[time](#column-method-time) -[timeTz](#column-method-timeTz) -[timestamp](#column-method-timestamp) -[timestamps](#column-method-timestamps) -[timestampsTz](#column-method-timestampsTz) -[softDeletes](#column-method-softDeletes) -[softDeletesTz](#column-method-softDeletesTz) -[year](#column-method-year) - -
    - -#### Binary Types - -
    - -[binary](#column-method-binary) - -
    - -#### Object & Json Types - -
    - -[json](#column-method-json) -[jsonb](#column-method-jsonb) - -
    - -#### UUID - -
    - -[uuid](#column-method-uuid) -[uuidMorphs](#column-method-uuidMorphs) - -
    - -#### Spatial Types - -
    - -[geometry](#column-method-geometry) - -
    - -#### Relationship Types - -
    - -[foreignId](#column-method-foreignId) -[foreignUlid](#column-method-foreignUlid) -[foreignUuid](#column-method-foreignUuid) -[morphs](#column-method-morphs) -[nullableMorphs](#column-method-nullableMorphs) - -
    - -#### Specialty Types - -
    - -[enum](#column-method-enum) -[set](#column-method-set) -[macAddress](#column-method-macAddress) -[ipAddress](#column-method-ipAddress) -[rememberToken](#column-method-rememberToken) -[vector](#column-method-vector) - -
    - -
    -#### bigIncrements() - -The `bigIncrements` method creates an auto-incrementing `UNSIGNED BIGINT` (primary key) equivalent column: - -```php -$table->bigIncrements('id'); -``` - - -#### bigInteger() - -The `bigInteger` method creates a `BIGINT` equivalent column: - -```php -$table->bigInteger('votes'); -``` - - -#### binary() - -The `binary` method creates a `BLOB` equivalent column: - -```php -$table->binary('photo'); -``` - -When utilizing MySQL, MariaDB, or SQL Server, you may pass `length` and `fixed` arguments to create `VARBINARY` or `BINARY` equivalent column: - -```php -$table->binary('data', length: 16); // VARBINARY(16) - -$table->binary('data', length: 16, fixed: true); // BINARY(16) -``` - - -#### boolean() - -The `boolean` method creates a `BOOLEAN` equivalent column: - -```php -$table->boolean('confirmed'); -``` - - -#### char() - -The `char` method creates a `CHAR` equivalent column with of a given length: - -```php -$table->char('name', length: 100); -``` - - -#### dateTimeTz() - -The `dateTimeTz` method creates a `DATETIME` (with timezone) equivalent column with an optional fractional seconds precision: - -```php -$table->dateTimeTz('created_at', precision: 0); -``` - - -#### dateTime() - -The `dateTime` method creates a `DATETIME` equivalent column with an optional fractional seconds precision: - -```php -$table->dateTime('created_at', precision: 0); -``` - - -#### date() - -The `date` method creates a `DATE` equivalent column: - -```php -$table->date('created_at'); -``` - - -#### decimal() - -The `decimal` method creates a `DECIMAL` equivalent column with the given precision (total digits) and scale (decimal digits): - -```php -$table->decimal('amount', total: 8, places: 2); -``` - - -#### double() - -The `double` method creates a `DOUBLE` equivalent column: - -```php -$table->double('amount'); -``` - - -#### enum() - -The `enum` method creates a `ENUM` equivalent column with the given valid values: - -```php -$table->enum('difficulty', ['easy', 'hard']); -``` - - -#### float() - -The `float` method creates a `FLOAT` equivalent column with the given precision: - -```php -$table->float('amount', precision: 53); -``` - - -#### foreignId() - -The `foreignId` method creates an `UNSIGNED BIGINT` equivalent column: - -```php -$table->foreignId('user_id'); -``` - - -#### foreignUlid() - -The `foreignUlid` method creates a `ULID` equivalent column: - -```php -$table->foreignUlid('user_id'); -``` - - -#### foreignUuid() - -The `foreignUuid` method creates a `UUID` equivalent column: - -```php -$table->foreignUuid('user_id'); -``` - - -#### geometry() - -The `geometry` method creates a `GEOMETRY` equivalent column with the given spatial type and SRID (Spatial Reference System Identifier): - -```php -$table->geometry('positions', subtype: 'point', srid: 0); -``` - -::: note -Support for spatial types depends on your database driver. Please refer to your database's documentation. If your application is utilizing a PostgreSQL database, you must install the [PostGIS](https://postgis.net) extension before the `geometry` method may be used. -::: - - -#### id() - -The `id` method is an alias of the `bigIncrements` method. By default, the method will create an `id` column; however, you may pass a column name if you would like to assign a different name to the column: - -```php -$table->id(); -``` - - -#### increments() - -The `increments` method creates an auto-incrementing `UNSIGNED INTEGER` equivalent column as a primary key: - -```php -$table->increments('id'); -``` - - -#### integer() - -The `integer` method creates an `INTEGER` equivalent column: - -```php -$table->integer('votes'); -``` - - -#### ipAddress() - -The `ipAddress` method creates a `VARCHAR` equivalent column: - -```php -$table->ipAddress('visitor'); -``` - -When using PostgreSQL, an `INET` column will be created. - - -#### json() - -The `json` method creates a `JSON` equivalent column: - -```php -$table->json('options'); -``` - -When using SQLite, a `TEXT` column will be created. - - -#### jsonb() - -The `jsonb` method creates a `JSONB` equivalent column: - -```php -$table->jsonb('options'); -``` - -When using SQLite, a `TEXT` column will be created. - - -#### longText() - -The `longText` method creates a `LONGTEXT` equivalent column: - -```php -$table->longText('description'); -``` - -When utilizing MySQL or MariaDB, you may apply a `binary` character set to the column in order to create a `LONGBLOB` equivalent column: - -```php -$table->longText('data')->charset('binary'); // LONGBLOB -``` - - -#### macAddress() - -The `macAddress` method creates a column that is intended to hold a MAC address. Some database systems, such as PostgreSQL, have a dedicated column type for this type of data. Other database systems will use a string equivalent column: - -```php -$table->macAddress('device'); -``` - - -#### mediumIncrements() - -The `mediumIncrements` method creates an auto-incrementing `UNSIGNED MEDIUMINT` equivalent column as a primary key: - -```php -$table->mediumIncrements('id'); -``` - - -#### mediumInteger() - -The `mediumInteger` method creates a `MEDIUMINT` equivalent column: - -```php -$table->mediumInteger('votes'); -``` - - -#### mediumText() - -The `mediumText` method creates a `MEDIUMTEXT` equivalent column: - -```php -$table->mediumText('description'); -``` - -When utilizing MySQL or MariaDB, you may apply a `binary` character set to the column in order to create a `MEDIUMBLOB` equivalent column: - -```php -$table->mediumText('data')->charset('binary'); // MEDIUMBLOB -``` - - -#### morphs() - -The `morphs` method is a convenience method that adds a `{column}_id` equivalent column and a `{column}_type` `VARCHAR` equivalent column. The column type for the `{column}_id` will be `UNSIGNED BIGINT`, `CHAR(36)`, or `CHAR(26)` depending on the model key type. - -This method is intended to be used when defining the columns necessary for a polymorphic [Eloquent relationship](/docs/eloquent-relationships). In the following example, `taggable_id` and `taggable_type` columns would be created: - -```php -$table->morphs('taggable'); -``` - - -#### nullableMorphs() - -The method is similar to the [morphs](#column-method-morphs) method; however, the columns that are created will be "nullable": - -```php -$table->nullableMorphs('taggable'); -``` - - -#### rememberToken() - -The `rememberToken` method creates a nullable, `VARCHAR(100)` equivalent column that is intended to store the current "remember me" [authentication token](/docs/authentication#remembering-users): - -```php -$table->rememberToken(); -``` - - -#### set() - -The `set` method creates a `SET` equivalent column with the given list of valid values: - -```php -$table->set('flavors', ['strawberry', 'vanilla']); -``` - - -#### smallIncrements() - -The `smallIncrements` method creates an auto-incrementing `UNSIGNED SMALLINT` equivalent column as a primary key: - -```php -$table->smallIncrements('id'); -``` - - -#### smallInteger() - -The `smallInteger` method creates a `SMALLINT` equivalent column: - -```php -$table->smallInteger('votes'); -``` - - -#### softDeletesTz() - -The `softDeletesTz` method adds a nullable `deleted_at` `TIMESTAMP` (with timezone) equivalent column with an optional fractional seconds precision. This column is intended to store the `deleted_at` timestamp needed for Eloquent's "soft delete" functionality: - -```php -$table->softDeletesTz('deleted_at', precision: 0); -``` - - -#### softDeletes() - -The `softDeletes` method adds a nullable `deleted_at` `TIMESTAMP` equivalent column with an optional fractional seconds precision. This column is intended to store the `deleted_at` timestamp needed for Eloquent's "soft delete" functionality: - -```php -$table->softDeletes('deleted_at', precision: 0); -``` - - -#### string() - -The `string` method creates a `VARCHAR` equivalent column of the given length: - -```php -$table->string('name', length: 100); -``` - - -#### text() - -The `text` method creates a `TEXT` equivalent column: - -```php -$table->text('description'); -``` - -When utilizing MySQL or MariaDB, you may apply a `binary` character set to the column in order to create a `BLOB` equivalent column: - -```php -$table->text('data')->charset('binary'); // BLOB -``` - - -#### timeTz() - -The `timeTz` method creates a `TIME` (with timezone) equivalent column with an optional fractional seconds precision: - -```php -$table->timeTz('sunrise', precision: 0); -``` - - -#### time() - -The `time` method creates a `TIME` equivalent column with an optional fractional seconds precision: - -```php -$table->time('sunrise', precision: 0); -``` - - -#### timestampTz() - -The `timestampTz` method creates a `TIMESTAMP` (with timezone) equivalent column with an optional fractional seconds precision: - -```php -$table->timestampTz('added_at', precision: 0); -``` - - -#### timestamp() - -The `timestamp` method creates a `TIMESTAMP` equivalent column with an optional fractional seconds precision: - -```php -$table->timestamp('added_at', precision: 0); -``` - - -#### timestampsTz() - -The `timestampsTz` method creates `created_at` and `updated_at` `TIMESTAMP` (with timezone) equivalent columns with an optional fractional seconds precision: - -```php -$table->timestampsTz(precision: 0); -``` - - -#### timestamps() - -The `timestamps` method creates `created_at` and `updated_at` `TIMESTAMP` equivalent columns with an optional fractional seconds precision: - -```php -$table->timestamps(precision: 0); -``` - - -#### tinyIncrements() - -The `tinyIncrements` method creates an auto-incrementing `UNSIGNED TINYINT` equivalent column as a primary key: - -```php -$table->tinyIncrements('id'); -``` - - -#### tinyInteger() - -The `tinyInteger` method creates a `TINYINT` equivalent column: - -```php -$table->tinyInteger('votes'); -``` - - -#### tinyText() - -The `tinyText` method creates a `TINYTEXT` equivalent column: - -```php -$table->tinyText('notes'); -``` - -When utilizing MySQL or MariaDB, you may apply a `binary` character set to the column in order to create a `TINYBLOB` equivalent column: - -```php -$table->tinyText('data')->charset('binary'); // TINYBLOB -``` - - -#### unsignedBigInteger() - -The `unsignedBigInteger` method creates an `UNSIGNED BIGINT` equivalent column: - -```php -$table->unsignedBigInteger('votes'); -``` - - -#### unsignedInteger() - -The `unsignedInteger` method creates an `UNSIGNED INTEGER` equivalent column: - -```php -$table->unsignedInteger('votes'); -``` - - -#### unsignedMediumInteger() - -The `unsignedMediumInteger` method creates an `UNSIGNED MEDIUMINT` equivalent column: - -```php -$table->unsignedMediumInteger('votes'); -``` - - -#### unsignedSmallInteger() - -The `unsignedSmallInteger` method creates an `UNSIGNED SMALLINT` equivalent column: - -```php -$table->unsignedSmallInteger('votes'); -``` - - -#### unsignedTinyInteger() - -The `unsignedTinyInteger` method creates an `UNSIGNED TINYINT` equivalent column: - -```php -$table->unsignedTinyInteger('votes'); -``` - - -#### uuidMorphs() - -The `uuidMorphs` method is a convenience method that adds a `{column}_id` `CHAR(36)` equivalent column and a `{column}_type` `VARCHAR` equivalent column. - -This method is intended to be used when defining the columns necessary for a polymorphic [Eloquent relationship](/docs/eloquent-relationships) that use UUID identifiers. In the following example, `taggable_id` and `taggable_type` columns would be created: - -```php -$table->uuidMorphs('taggable'); -``` - - -#### ulid() - -The `ulid` method creates a `ULID` equivalent column: - -```php -$table->ulid('id'); -``` - - -#### uuid() - -The `uuid` method creates a `UUID` equivalent column: - -```php -$table->uuid('id'); -``` - - -#### vector() - -The `vector` method creates a `vector` equivalent column: - -```php -$table->vector('embedding', dimensions: 100); -``` - - -#### year() - -The `year` method creates a `YEAR` equivalent column: - -```php -$table->year('birth_year'); -``` - -### Column Modifiers - -In addition to the column types listed above, there are several column "modifiers" you may use when adding a column to a database table. For example, to make the column "nullable", you may use the `nullable` method: - -```php -use Hyperf\Database\Schema\Blueprint; -use Hypervel\Support\Facades\Schema; - -Schema::table('users', function (Blueprint $table) { - $table->string('email')->nullable(); -}); -``` - -The following table contains all of the available column modifiers. This list does not include [index modifiers](#creating-indexes): - -
    - -| Modifier | Description | -| ----------------------------------- | ---------------------------------------------------------------------------------------------- | -| `->after('column')` | Place the column "after" another column (MariaDB / MySQL). | -| `->autoIncrement()` | Set `INTEGER` columns as auto-incrementing (primary key). | -| `->charset('utf8mb4')` | Specify a character set for the column (MariaDB / MySQL). | -| `->collation('utf8mb4_unicode_ci')` | Specify a collation for the column. | -| `->comment('my comment')` | Add a comment to a column (MariaDB / MySQL / PostgreSQL). | -| `->default($value)` | Specify a "default" value for the column. | -| `->first()` | Place the column "first" in the table (MariaDB / MySQL). | -| `->from($integer)` | Set the starting value of an auto-incrementing field (MariaDB / MySQL / PostgreSQL). | -| `->invisible()` | Make the column "invisible" to `SELECT *` queries (MariaDB / MySQL). | -| `->nullable($value = true)` | Allow `NULL` values to be inserted into the column. | -| `->storedAs($expression)` | Create a stored generated column (MariaDB / MySQL / PostgreSQL / SQLite). | -| `->unsigned()` | Set `INTEGER` columns as `UNSIGNED` (MariaDB / MySQL). | -| `->useCurrent()` | Set `TIMESTAMP` columns to use `CURRENT_TIMESTAMP` as default value. | -| `->useCurrentOnUpdate()` | Set `TIMESTAMP` columns to use `CURRENT_TIMESTAMP` when a record is updated (MariaDB / MySQL). | -| `->virtualAs($expression)` | Create a virtual generated column (MariaDB / MySQL / SQLite). | -| `->generatedAs($expression)` | Create an identity column with specified sequence options (PostgreSQL). | -| `->always()` | Defines the precedence of sequence values over input for an identity column (PostgreSQL). | - -
    - -#### Default Expressions - -The `default` modifier accepts a value or an `Hyperf\Database\Query\Expression` instance. Using an `Expression` instance will prevent Hypervel from wrapping the value in quotes and allow you to use database specific functions. One situation where this is particularly useful is when you need to assign default values to JSON columns: - -```php -id(); - $table->json('movies')->default(new Expression('(JSON_ARRAY())')); - $table->timestamps(); - }); - } -}; -``` - -::: warning -Support for default expressions depends on your database driver, database version, and the field type. Please refer to your database's documentation. -::: - -#### Column Order - -When using the MariaDB or MySQL database, the `after` method may be used to add columns after an existing column in the schema: - -```php -$table->after('password', function (Blueprint $table) { - $table->string('address_line1'); - $table->string('address_line2'); - $table->string('city'); -}); -``` - -### Modifying Columns - -The `change` method allows you to modify the type and attributes of existing columns. For example, you may wish to increase the size of a `string` column. To see the `change` method in action, let's increase the size of the `name` column from 25 to 50. To accomplish this, we simply define the new state of the column and then call the `change` method: - -```php -Schema::table('users', function (Blueprint $table) { - $table->string('name', 50)->change(); -}); -``` - -When modifying a column, you must explicitly include all the modifiers you want to keep on the column definition - any missing attribute will be dropped. For example, to retain the `unsigned`, `default`, and `comment` attributes, you must call each modifier explicitly when changing the column: - -```php -Schema::table('users', function (Blueprint $table) { - $table->integer('votes')->unsigned()->default(1)->comment('my comment')->change(); -}); -``` - -The `change` method does not change the indexes of the column. Therefore, you may use index modifiers to explicitly add or drop an index when modifying the column: - -```php -// Add an index... -$table->bigIncrements('id')->primary()->change(); - -// Drop an index... -$table->char('postal_code', 10)->unique(false)->change(); -``` - -### Renaming Columns - -To rename a column, you may use the `renameColumn` method provided by the schema builder: - -```php -Schema::table('users', function (Blueprint $table) { - $table->renameColumn('from', 'to'); -}); -``` - -### Dropping Columns - -To drop a column, you may use the `dropColumn` method on the schema builder: - -```php -Schema::table('users', function (Blueprint $table) { - $table->dropColumn('votes'); -}); -``` - -You may drop multiple columns from a table by passing an array of column names to the `dropColumn` method: - -```php -Schema::table('users', function (Blueprint $table) { - $table->dropColumn(['votes', 'avatar', 'location']); -}); -``` - -#### Available Command Aliases - -Hypervel provides several convenient methods related to dropping common types of columns. Each of these methods is described in the table below: - -
    - -| Command | Description | -| ----------------------------------- | ----------------------------------------------------- | -| `$table->dropMorphs('morphable');` | Drop the `morphable_id` and `morphable_type` columns. | -| `$table->dropRememberToken();` | Drop the `remember_token` column. | -| `$table->dropSoftDeletes();` | Drop the `deleted_at` column. | -| `$table->dropSoftDeletesTz();` | Alias of `dropSoftDeletes()` method. | -| `$table->dropTimestamps();` | Drop the `created_at` and `updated_at` columns. | -| `$table->dropTimestampsTz();` | Alias of `dropTimestamps()` method. | - -
    - -## Indexes - -### Creating Indexes - -The Hypervel schema builder supports several types of indexes. The following example creates a new `email` column and specifies that its values should be unique. To create the index, we can chain the `unique` method onto the column definition: - -```php -use Hyperf\Database\Schema\Blueprint; -use Hypervel\Support\Facades\Schema; - -Schema::table('users', function (Blueprint $table) { - $table->string('email')->unique(); -}); -``` - -Alternatively, you may create the index after defining the column. To do so, you should call the `unique` method on the schema builder blueprint. This method accepts the name of the column that should receive a unique index: - -```php -$table->unique('email'); -``` - -You may even pass an array of columns to an index method to create a compound (or composite) index: - -```php -$table->index(['account_id', 'created_at']); -``` - -When creating an index, Hypervel will automatically generate an index name based on the table, column names, and the index type, but you may pass a second argument to the method to specify the index name yourself: - -```php -$table->unique('email', 'unique_email'); -``` - -#### Available Index Types - -Hypervel's schema builder blueprint class provides methods for creating each type of index supported by Hypervel. Each index method accepts an optional second argument to specify the name of the index. If omitted, the name will be derived from the names of the table and column(s) used for the index, as well as the index type. Each of the available index methods is described in the table below: - -
    - -| Command | Description | -| ------------------------------------------------ | -------------------------------------------------------------- | -| `$table->primary('id');` | Adds a primary key. | -| `$table->primary(['id', 'parent_id']);` | Adds composite keys. | -| `$table->unique('email');` | Adds a unique index. | -| `$table->index('state');` | Adds an index. | -| `$table->fullText('body');` | Adds a full text index (MariaDB / MySQL / PostgreSQL). | -| `$table->fullText('body')->language('english');` | Adds a full text index of the specified language (PostgreSQL). | -| `$table->spatialIndex('location');` | Adds a spatial index (except SQLite). | - -
    - - -### Renaming Indexes - -To rename an index, you may use the `renameIndex` method provided by the schema builder blueprint. This method accepts the current index name as its first argument and the desired name as its second argument: - -```php -$table->renameIndex('from', 'to') -``` - -### Dropping Indexes - -To drop an index, you must specify the index's name. By default, Hypervel automatically assigns an index name based on the table name, the name of the indexed column, and the index type. Here are some examples: - -
    - -| Command | Description | -| -------------------------------------------------------- | ----------------------------------------------------------- | -| `$table->dropPrimary('users_id_primary');` | Drop a primary key from the "users" table. | -| `$table->dropUnique('users_email_unique');` | Drop a unique index from the "users" table. | -| `$table->dropIndex('geo_state_index');` | Drop a basic index from the "geo" table. | -| `$table->dropFullText('posts_body_fulltext');` | Drop a full text index from the "posts" table. | -| `$table->dropSpatialIndex('geo_location_spatialindex');` | Drop a spatial index from the "geo" table (except SQLite). | - -
    - -If you pass an array of columns into a method that drops indexes, the conventional index name will be generated based on the table name, columns, and index type: - -```php -Schema::table('geo', function (Blueprint $table) { - $table->dropIndex(['state']); // Drops index 'geo_state_index' -}); -``` - -### Foreign Key Constraints - -Hypervel also provides support for creating foreign key constraints, which are used to force referential integrity at the database level. For example, let's define a `user_id` column on the `posts` table that references the `id` column on a `users` table: - -```php -use Hyperf\Database\Schema\Blueprint; -use Hypervel\Support\Facades\Schema; - -Schema::table('posts', function (Blueprint $table) { - $table->unsignedBigInteger('user_id'); - - $table->foreign('user_id')->references('id')->on('users'); -}); -``` - -Since this syntax is rather verbose, Hypervel provides additional, terser methods that use conventions to provide a better developer experience. When using the `foreignId` method to create your column, the example above can be rewritten like so: - -```php -Schema::table('posts', function (Blueprint $table) { - $table->foreignId('user_id')->constrained(); -}); -``` - -The `foreignId` method creates an `UNSIGNED BIGINT` equivalent column, while the `constrained` method will use conventions to determine the table and column being referenced. If your table name does not match Hypervel's conventions, you may manually provide it to the `constrained` method. In addition, the name that should be assigned to the generated index may be specified as well: - -```php -Schema::table('posts', function (Blueprint $table) { - $table->foreignId('user_id')->constrained( - table: 'users', indexName: 'posts_user_id' - ); -}); -``` - -You may also specify the desired action for the "on delete" and "on update" properties of the constraint: - -```php -$table->foreignId('user_id') - ->constrained() - ->onUpdate('cascade') - ->onDelete('cascade'); -``` - -An alternative, expressive syntax is also provided for these actions: - -
    - -| Method | Description | -| ----------------------------- | ------------------------------------------------- | -| `$table->cascadeOnUpdate();` | Updates should cascade. | -| `$table->restrictOnUpdate();` | Updates should be restricted. | -| `$table->nullOnUpdate();` | Updates should set the foreign key value to null. | -| `$table->noActionOnUpdate();` | No action on updates. | -| `$table->cascadeOnDelete();` | Deletes should cascade. | -| `$table->restrictOnDelete();` | Deletes should be restricted. | -| `$table->nullOnDelete();` | Deletes should set the foreign key value to null. | -| `$table->noActionOnDelete();` | Prevents deletes if child records exist. | - -
    - -Any additional [column modifiers](#column-modifiers) must be called before the `constrained` method: - -```php -$table->foreignId('user_id') - ->nullable() - ->constrained(); -``` - -#### Dropping Foreign Keys - -To drop a foreign key, you may use the `dropForeign` method, passing the name of the foreign key constraint to be deleted as an argument. Foreign key constraints use the same naming convention as indexes. In other words, the foreign key constraint name is based on the name of the table and the columns in the constraint, followed by a "\_foreign" suffix: - -```php -$table->dropForeign('posts_user_id_foreign'); -``` - -Alternatively, you may pass an array containing the column name that holds the foreign key to the `dropForeign` method. The array will be converted to a foreign key constraint name using Hypervel's constraint naming conventions: - -```php -$table->dropForeign(['user_id']); -``` - -#### Toggling Foreign Key Constraints - -You may enable or disable foreign key constraints within your migrations by using the following methods: - -```php -Schema::enableForeignKeyConstraints(); - -Schema::disableForeignKeyConstraints(); - -Schema::withoutForeignKeyConstraints(function () { - // Constraints disabled within this closure... -}); -``` - -::: warning -SQLite disables foreign key constraints by default. When using SQLite, make sure to [enable foreign key support](/docs/database#configuration) in your database configuration before attempting to create them in your migrations. -::: \ No newline at end of file diff --git a/_archive/docs/0.3/docs/mocking.md b/_archive/docs/0.3/docs/mocking.md deleted file mode 100644 index 51057e124a..0000000000 --- a/_archive/docs/0.3/docs/mocking.md +++ /dev/null @@ -1,470 +0,0 @@ -# Mocking -[[toc]] - -## Introduction - -When testing Hypervel applications, you may wish to "mock" certain aspects of your application so they are not actually executed during a given test. For example, when testing a controller that dispatches an event, you may wish to mock the event listeners so they are not actually executed during the test. This allows you to only test the controller's HTTP response without worrying about the execution of the event listeners, since the event listeners can be tested in their own test case. - -Hypervel provides helpers for mocking events, jobs, and facades out of the box. These helpers primarily provide a convenience layer over Mockery so you do not have to manually make complicated Mockery method calls. You can also use [Mockery](http://docs.mockery.io/en/latest/) or PHPUnit to create your own mocks or spies. - -## Mocking Objects - -When mocking an object that is going to be injected into your application via Hypervel's service container, you will need to bind your mocked instance into the container as an `instance` binding. This will instruct the container to use your mocked instance of the object instead of constructing the object itself: - -```php -use App\Service; -use Mockery; - -$this->instance(Service::class, Mockery::mock(Service::class, function ($mock) { - $mock->shouldReceive('process')->once(); -})); -``` - -In order to make this more convenient, you may use the `mock` method, which is provided by Hypervel's base test case class: - -```php -use App\Service; - -$this->mock(Service::class, function ($mock) { - $mock->shouldReceive('process')->once(); -}); -``` - -You may use the `partialMock` method when you only need to mock a few methods of an object. The methods that are not mocked will be executed normally when called: - -```php -use App\Service; - -$this->partialMock(Service::class, function ($mock) { - $mock->shouldReceive('process')->once(); -}); -``` - -Similarly, if you want to spy on an object, Hypervel's base test case class offers a `spy` method as a convenient wrapper around the `Mockery::spy` method: - -```php -use App\Service; - -$this->spy(Service::class, function ($mock) { - $mock->shouldHaveReceived('process'); -}); -``` - -## Bus Fake - -As an alternative to mocking, you may use the `Bus` facade's `fake` method to prevent jobs from being dispatched. When using fakes, assertions are made after the code under test is executed: - -```php -order->id === $order->id; - }); - - // Assert a job was not dispatched... - Bus::assertNotDispatched(AnotherJob::class); - } -} -``` - -## Event Fake - -As an alternative to mocking, you may use the `Event` facade's `fake` method to prevent all event listeners from executing. You may then assert that events were dispatched and even inspect the data they received. When using fakes, assertions are made after the code under test is executed: - -```php -order->id === $order->id; - }); - - // Assert an event was dispatched twice... - Event::assertDispatched(OrderShipped::class, 2); - - // Assert an event was not dispatched... - Event::assertNotDispatched(OrderFailedToShip::class); - } -} -``` - -::: note -After calling `Event::fake()`, no event listeners will be executed. So, if your tests use model factories that rely on events, such as creating a UUID during a model's `creating` event, you should call `Event::fake()` **after** using your factories. -::: - -#### Faking A Subset Of Events - -If you only want to fake event listeners for a specific set of events, you may pass them to the `fake` or `fakeFor` method: - -```php -/** - * Test order process. - */ -public function testOrderProcess() -{ - Event::fake([ - OrderCreated::class, - ]); - - $order = factory(Order::class)->create(); - - Event::assertDispatched(OrderCreated::class); - - // Other events are dispatched as normal... - $order->update([...]); -} -``` - -### Scoped Event Fakes - -If you only want to fake event listeners for a portion of your test, you may use the `fakeFor` method: - -```php -create(); - - Event::assertDispatched(OrderCreated::class); - - return $order; - }); - - // Events are dispatched as normal and observers will run ... - $order->update([...]); - } -} -``` - -## HTTP Fake - -The `Http` facade's `fake` method allows you to instruct the HTTP client to return stubbed / dummy responses when requests are made. For more information on faking outgoing HTTP requests, please consult the [HTTP Client testing documentation](/docs/http-client#testing). - -## Mail Fake - -You may use the `Mail` facade's `fake` method to prevent mail from being sent. You may then assert that [mailables](/docs/mail) were sent to users and even inspect the data they received. When using fakes, assertions are made after the code under test is executed: - -```php -order->id === $order->id; - }); - - // Assert a message was sent to the given users... - Mail::assertSent(OrderShipped::class, function ($mail) use ($user) { - return $mail->hasTo($user->email) && - $mail->hasCc('...') && - $mail->hasBcc('...'); - }); - - // Assert a mailable was sent twice... - Mail::assertSent(OrderShipped::class, 2); - - // Assert a mailable was not sent... - Mail::assertNotSent(AnotherMailable::class); - } -} -``` - -If you are queueing mailables for delivery in the background, you should use the `assertQueued` method instead of `assertSent`: - -```php -Mail::assertQueued(...); -Mail::assertNotQueued(...); -``` - -## Notification Fake - -You may use the `Notification` facade's `fake` method to prevent notifications from being sent. You may then assert that [notifications](/docs/notifications) were sent to users and even inspect the data they received. When using fakes, assertions are made after the code under test is executed: - -```php -order->id === $order->id; - } - ); - - // Assert a notification was sent to the given users... - Notification::assertSentTo( - [$user], OrderShipped::class - ); - - // Assert a notification was not sent... - Notification::assertNotSentTo( - [$user], AnotherNotification::class - ); - - // Assert a notification was sent via Notification::route() method... - Notification::assertSentTo( - new AnonymousNotifiable, OrderShipped::class - ); - - // Assert Notification::route() method sent notification to the correct user... - Notification::assertSentTo( - new AnonymousNotifiable, - OrderShipped::class, - function ($notification, $channels, $notifiable) use ($user) { - return $notifiable->routes['mail'] === $user->email; - } - ); - } -} -``` - -## Queue Fake - -As an alternative to mocking, you may use the `Queue` facade's `fake` method to prevent jobs from being queued. You may then assert that jobs were pushed to the queue and even inspect the data they received. When using fakes, assertions are made after the code under test is executed: - -```php -order->id === $order->id; - }); - - // Assert a job was pushed to a given queue... - Queue::assertPushedOn('queue-name', ShipOrder::class); - - // Assert a job was pushed twice... - Queue::assertPushed(ShipOrder::class, 2); - - // Assert a job was not pushed... - Queue::assertNotPushed(AnotherJob::class); - - // Assert a job was pushed with a given chain of jobs, matching by class... - Queue::assertPushedWithChain(ShipOrder::class, [ - AnotherJob::class, - FinalJob::class - ]); - - // Assert a job was pushed with a given chain of jobs, matching by both class and properties... - Queue::assertPushedWithChain(ShipOrder::class, [ - new AnotherJob('foo'), - new FinalJob('bar'), - ]); - - // Assert a job was pushed without a chain of jobs... - Queue::assertPushedWithoutChain(ShipOrder::class); - } -} -``` - -## Storage Fake - -The `Storage` facade's `fake` method allows you to easily generate a fake disk that, combined with the file generation utilities of the `UploadedFile` class, greatly simplifies the testing of file uploads. For example: - -```php -json('POST', '/photos', [ - UploadedFile::fake()->image('photo1.jpg'), - UploadedFile::fake()->image('photo2.jpg') - ]); - - // Assert one or more files were stored... - Storage::disk('photos')->assertExists('photo1.jpg'); - Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']); - - // Assert one or more files were not stored... - Storage::disk('photos')->assertMissing('missing.jpg'); - Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']); - } -} -``` - -::: tip -By default, the `fake` method will delete all files in its temporary directory. If you would like to keep these files, you may use the "persistentFake" method instead. -::: - -## Facades - -Unlike traditional static method calls, [facades](/docs/facades) may be mocked. This provides a great advantage over traditional static methods and grants you the same testability you would have if you were using dependency injection. When testing, you may often want to mock a call to a Hypervel facade in one of your controllers. For example, consider the following controller action: - -```php -once() - ->with('key') - ->andReturn('value'); - - $response = $this->get('/users'); - - // ... - } -} -``` - -::: note -You should not mock the `Request` facade. Instead, pass the input you desire into the HTTP helper methods such as `get` and `post` when running your test. Likewise, instead of mocking the `Config` facade, call the `Config::set` method in your tests. -::: \ No newline at end of file diff --git a/_archive/docs/0.3/docs/nested-set.md b/_archive/docs/0.3/docs/nested-set.md deleted file mode 100644 index 12acc52005..0000000000 --- a/_archive/docs/0.3/docs/nested-set.md +++ /dev/null @@ -1,727 +0,0 @@ -# Hypervel Nested Set - -[[toc]] - -## Introduction - -Most web applications need to store and work with hierarchical data at some point. Whether you're building a menu system, organizing product categories, or managing organizational structures, you'll eventually face the challenge of efficiently querying tree-like data in a relational database. - -The Hypervel Nested Set package provides a robust solution for managing hierarchical data using the nested set model - a technique that stores tree structures in a way that makes retrieving entire subtrees, checking ancestry relationships, and performing complex hierarchical queries remarkably efficient. This package is ported from [Lazychaser's Nested Set package](https://github.com/lazychaser/laravel-nestedset) and optimized for the Hypervel framework. Unlike traditional parent-child relationships that require recursive queries or multiple database hits, nested sets allow you to fetch an entire category tree or determine if a node is an ancestor of another with a single, simple query. - -This package handles all the complexity of maintaining the tree structure behind the scenes. When you move a node or insert new children, the package automatically recalculates the internal boundaries that make fast queries possible. You can focus on your application logic while the package ensures your tree data remains consistent and performant. - -### When to Use Nested Set - -**Best for:** -- Menu systems and navigation hierarchies -- Category trees (product categories, content taxonomies) -- Organizational charts and reporting structures -- Comment threads and discussions -- File/folder hierarchies -- Geographic hierarchies (country > state > city) - -**Consider alternatives when:** -- Tree structure changes frequently (many insertions/updates) -- You primarily need parent-child relationships without deep querying -- Simple adjacency lists are sufficient for your use case - -## Installation - -Install the package via Composer: - -```bash -composer require hypervel/nested-set -``` - -## Setup - -### Database Migration - -Add the nested set columns to your table using the provided helper: - -```php -use Hyperf\Database\Schema\Blueprint; -use Hypervel\Database\Migrations\Migration; -use Hypervel\NestedSet\NestedSet; -use Hypervel\Support\Facades\Schema; - -return new class extends Migration -{ - public function up(): void - { - Schema::create('categories', function (Blueprint $table) { - $table->id(); - $table->string('name'); - $table->text('description')->nullable(); - - // Add nested set columns - NestedSet::columns($table); - - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::table('categories', function (Blueprint $table) { - NestedSet::dropColumns($table); - }); - - Schema::dropIfExists('categories'); - } -}; -``` - -This creates the following columns: -- `_lft` (unsigned integer) - Left boundary -- `_rgt` (unsigned integer) - Right boundary -- `parent_id` (unsigned integer, nullable) - Parent node ID - -### Model Setup - -Add the `HasNode` trait to your model: - -```php -namespace App\Models; - -use Hypervel\Database\Eloquent\Model; -use Hypervel\NestedSet\HasNode; - -class Category extends Model -{ - use HasNode; - - protected array $fillable = [ - 'name', - 'description', - ]; -} -``` - -## Basic Usage - -### Creating Root Nodes - -```php -use App\Models\Category; - -// Create a root node -$electronics = Category::create([ - 'name' => 'Electronics' -]); - -// Or explicitly make it root -$clothing = new Category(['name' => 'Clothing']); -$clothing->saveAsRoot(); -``` - -### Creating Child Nodes - -```php -// Append to parent (becomes last child) -$computers = new Category(['name' => 'Computers']); -$electronics->appendNode($computers); - -// Prepend to parent (becomes first child) -$phones = new Category(['name' => 'Phones']); -$electronics->prependNode($phones); - -// Alternative syntax -$laptops = Category::create(['name' => 'Laptops']); -$laptops->appendToNode($computers); - -// Create with parent relationship -$tablets = Category::create([ - 'name' => 'Tablets', -], $electronics); -``` - -### Moving Nodes - -```php -// Move node to different parent -$laptops->appendToNode($electronics); - -// Insert before another node -$smartphones = Category::create(['name' => 'Smartphones']); -$smartphones->beforeNode($tablets); - -// Insert after another node -$smartwatches = Category::create(['name' => 'Smartwatches']); -$smartwatches->afterNode($smartphones); - -// Move up/down among siblings -$laptops->up(); // Move up one position -$tablets->down(2); // Move down two positions -``` - -## Retrieving Nodes - -### Basic Relationships - -```php -$category = Category::find(1); - -// Get immediate parent -$parent = $category->parent; - -// Get immediate children -$children = $category->children; - -// Check if node has relationships -if ($category->isRoot()) { - // This is a root node -} - -if ($category->isLeaf()) { - // This node has no children -} -``` - -### Ancestors and Descendants - -```php -// Get all ancestors (parents, grandparents, etc.) -$ancestors = $category->ancestors()->get(); -$ancestors = $category->getAncestors(); // Alternative - -// Get ancestors including self -$ancestorsAndSelf = $category->ancestorsAndSelf(); - -// Get all descendants (children, grandchildren, etc.) -$descendants = $category->descendants()->get(); -$descendants = $category->getDescendants(); // Alternative - -// Get descendants including self -$descendantsAndSelf = $category->descendantsAndSelf(); - -// Get only direct children -$directChildren = $category->children()->get(); -``` - -### Siblings - -```php -// Get siblings (nodes with same parent) -$siblings = $category->siblings()->get(); -$siblings = $category->getSiblings(); // Alternative - -// Get siblings including self -$siblingsAndSelf = $category->getSiblingsAndSelf(); - -// Get next/previous siblings -$nextSiblings = $category->getNextSiblings(); -$prevSiblings = $category->getPrevSiblings(); - -// Get immediate next/previous sibling -$nextSibling = $category->getNextSibling(); -$prevSibling = $category->getPrevSibling(); -``` - -### Advanced Queries - -```php -// Get root nodes only -$roots = Category::whereIsRoot()->get(); - -// Get leaf nodes only (no children) -$leaves = Category::whereIsLeaf()->get(); - -// Get nodes with children -$parents = Category::hasChildren()->get(); - -// Get ancestors of specific node -$ancestors = Category::ancestorsOf($categoryId); - -// Get descendants of specific node -$descendants = Category::descendantsOf($categoryId); - -// Get nodes after specific node -$afterNodes = Category::whereIsAfter($category)->get(); - -// Get nodes before specific node -$beforeNodes = Category::whereIsBefore($category)->get(); -``` - -## Tree Operations - -### Building Trees - -#### From Nested Arrays - -```php -$treeData = [ - 'name' => 'Electronics', - 'children' => [ - [ - 'name' => 'Computers', - 'children' => [ - ['name' => 'Laptops'], - ['name' => 'Desktops'], - ] - ], - [ - 'name' => 'Phones', - 'children' => [ - ['name' => 'iPhones'], - ['name' => 'Android Phones'], - ] - ] - ] -]; - -// Create entire tree structure -$electronics = Category::create($treeData); -``` - -#### Rebuilding Trees - -```php -// Rebuild tree from array data -$categories = [ - ['id' => 1, 'name' => 'Electronics', 'parent_id' => null], - ['id' => 2, 'name' => 'Computers', 'parent_id' => 1], - ['id' => 3, 'name' => 'Laptops', 'parent_id' => 2], - ['id' => 4, 'name' => 'Phones', 'parent_id' => 1], -]; - -Category::rebuildTree($categories); - -// Rebuild specific subtree -Category::rebuildSubtree($electronics, $subtreeData); -``` - -### Tree Collections - -The package provides specialized collection methods for working with tree data: - -```php -$categories = Category::get(); - -// Link parent-child relationships -$categories->linkNodes(); - -// Convert flat collection to nested tree -$tree = $categories->toTree(); - -// Convert to flat tree (ordered hierarchy) -$flatTree = $categories->toFlatTree(); -``` - -### Tree Validation and Repair - -```php -// Check for tree errors -$errors = Category::countErrors(); -/* -Returns array with error counts: -[ - 'oddness' => 0, // Invalid lft/rgt values - 'duplicates' => 0, // Duplicate lft/rgt values - 'wrong_parent' => 0, // Incorrect parent relationships - 'missing_parent' => 0 // Missing parent nodes -] -*/ - -// Get total error count -$totalErrors = Category::getTotalErrors(); - -// Check if tree is broken -if (Category::isBroken()) { - // Fix the entire tree - $fixed = Category::fixTree(); - echo "Fixed {$fixed} nodes"; -} - -// Fix specific subtree -$fixed = Category::fixSubtree($rootNode); -``` - -## Advanced Features - -### Depth Information - -```php -// Include depth level in results -$categoriesWithDepth = Category::withDepth()->get(); - -foreach ($categoriesWithDepth as $category) { - echo str_repeat(' ', $category->depth) . $category->name; -} - -// Alternative: calculate depth manually -$depth = $category->ancestors()->count(); -``` - -### Scoped Trees - -For multiple independent trees in the same table: - -```php -class Category extends Model -{ - use HasNode; - - // Define scope attributes - protected function getScopeAttributes(): array - { - return ['company_id']; // Separate trees per company - } -} - -// Usage -$companyCategories = Category::scoped(['company_id' => 1]) - ->defaultOrder() - ->get(); - -// All operations automatically respect scope -$electronics = Category::create([ - 'name' => 'Electronics', - 'company_id' => 1 -]); - -$computers = Category::create([ - 'name' => 'Computers', - 'company_id' => 1 -]); - -$computers->appendToNode($electronics); // Only works within same scope -``` - -### Soft Delete Support - -The package automatically handles soft deletes: - -```php -class Category extends Model -{ - use HasNode, SoftDeletes; -} - -// Deleting a node soft-deletes all descendants -$electronics->delete(); // Soft deletes entire subtree - -// Restoring a node restores all descendants deleted with it -$electronics->restore(); // Restores entire subtree - -// Force delete removes from tree structure -$electronics->forceDelete(); // Permanently removes and adjusts tree -``` - -### Custom Node Operations - -```php -class Category extends Model -{ - use HasNode; - - // Custom method using tree relationships - public function getPath(string $separator = ' > '): string - { - return $this->ancestorsAndSelf() - ->pluck('name') - ->implode($separator); - } - - // Get total product count including subcategories - public function getTotalProductCount(): int - { - return $this->descendantsAndSelf() - ->withCount('products') - ->sum('products_count'); - } - - // Check if category can be deleted - public function canDelete(): bool - { - return $this->products()->count() === 0 && - $this->descendants()->count() === 0; - } -} - -// Usage -echo $category->getPath(); // "Electronics > Computers > Laptops" -$totalProducts = $category->getTotalProductCount(); -``` - -## Query Optimization - -### Eager Loading - -```php -// Load tree with relationships -$categories = Category::with([ - 'children', - 'parent', - 'products' -])->get(); - -// Load ancestors for multiple nodes efficiently -$categories = Category::with('ancestors')->get(); -``` - -### Ordering Results - -```php -// Default tree order (by left boundary) -$categories = Category::defaultOrder()->get(); - -// Reverse order -$categories = Category::defaultOrder('desc')->get(); -// or -$categories = Category::reversed()->get(); - -// Custom ordering with depth -$categories = Category::withDepth() - ->orderBy('depth') - ->orderBy('name') - ->get(); -``` - -### Limiting Results - -```php -// Get only root level -$roots = Category::whereIsRoot() - ->withDepth() - ->where('depth', 0) - ->get(); - -// Get specific depth levels -$topTwoLevels = Category::withDepth() - ->where('depth', '<=', 2) - ->get(); - -// Exclude root from results -$nonRoots = Category::withoutRoot()->get(); -// or -$hasParent = Category::hasParent()->get(); -``` - -## Tree Rendering Examples - -### Simple Nested List - -```php -function renderTree($nodes, $depth = 0): string -{ - $html = $depth === 0 ? '
      ' : '
        '; - - foreach ($nodes as $node) { - $html .= '
      • '; - $html .= '' . $node->name . ''; - - if ($node->children->isNotEmpty()) { - $html .= renderTree($node->children, $depth + 1); - } - - $html .= '
      • '; - } - - $html .= '
      '; - return $html; -} - -// Usage -$tree = Category::with('children')->whereIsRoot()->get(); -echo renderTree($tree); -``` - -### Breadcrumb Navigation - -```php -function renderBreadcrumb($category): string -{ - $ancestors = $category->ancestorsAndSelf()->get(); - - $breadcrumb = ''; - return $breadcrumb; -} -``` - -### Tree Select Dropdown - -```php -function renderSelectOptions($categories, $selectedId = null): string -{ - $options = ''; - - foreach ($categories as $category) { - $indent = str_repeat('   ', $category->depth); - $selected = $category->id == $selectedId ? ' selected' : ''; - - $options .= ''; - } - - return $options; -} - -// Usage -$allCategories = Category::withDepth()->defaultOrder()->get(); -echo ''; -``` - -## Performance Considerations - -### Advantages of Nested Set - -- **Fast subtree queries**: Get all descendants in a single query -- **Efficient ancestor paths**: Get full path with one query -- **Simple depth calculations**: Depth is calculable from tree structure -- **Fast leaf detection**: Leaves are `rgt = lft + 1` - -### Disadvantages - -- **Complex updates**: Moving nodes requires updating multiple records -- **Insert overhead**: Adding nodes requires updating many `lft`/`rgt` values -- **Storage overhead**: Requires two additional integer columns - -### Optimization Tips - -1. **Use appropriate indexes**: -```sql --- Add indexes for performance -CREATE INDEX idx_lft_rgt ON categories (_lft, _rgt); -CREATE INDEX idx_parent_id ON categories (parent_id); -``` - -2. **Batch operations**: -```php -// Instead of multiple single inserts, use batch operations -Category::rebuildTree($treeData); -``` - -3. **Cache frequently accessed trees**: -```php -// Cache menu trees that don't change often -$menu = Cache::remember('main-menu', 3600, function () { - return Category::whereIn('type', ['menu']) - ->with('children') - ->whereIsRoot() - ->get(); -}); -``` - -4. **Use scoped queries when possible**: -```php -// Limit queries to specific scopes -$userCategories = Category::scoped(['user_id' => $userId]) - ->defaultOrder() - ->get(); -``` - -## Common Patterns - -### Menu System - -```php -class MenuItem extends Model -{ - use HasNode; - - protected array $fillable = [ - 'title', 'url', 'icon', 'is_active' - ]; - - // Get active menu items only - public function scopeActive($query) - { - return $query->where('is_active', true); - } - - // Get menu with all active descendants - public function getActiveMenu() - { - return $this->descendants() - ->active() - ->with('children') - ->get() - ->toTree(); - } -} -``` - -### Forum Categories - -```php -class ForumCategory extends Model -{ - use HasNode; - - public function topics() - { - return $this->hasMany(Topic::class); - } - - public function posts() - { - return $this->hasManyThrough(Post::class, Topic::class); - } - - // Get stats including subcategories - public function getStatsAttribute(): array - { - $descendants = $this->descendantsAndSelf()->pluck('id'); - - return [ - 'topics_count' => Topic::whereIn('category_id', $descendants)->count(), - 'posts_count' => Post::whereHas('topic', function ($query) use ($descendants) { - $query->whereIn('category_id', $descendants); - })->count(), - ]; - } -} -``` - -### Product Categories with Filters - -```php -class ProductCategory extends Model -{ - use HasNode; - - public function products() - { - return $this->hasMany(Product::class, 'category_id'); - } - - public function filters() - { - return $this->hasMany(CategoryFilter::class); - } - - // Get all products in category and subcategories - public function getAllProducts() - { - $categoryIds = $this->descendantsAndSelf()->pluck('id'); - - return Product::whereIn('category_id', $categoryIds); - } - - // Get inherited filters from ancestors - public function getAvailableFilters() - { - $ancestorIds = $this->ancestorsAndSelf()->pluck('id'); - - return CategoryFilter::whereIn('category_id', $ancestorIds) - ->where('is_active', true) - ->get(); - } -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/notifications.md b/_archive/docs/0.3/docs/notifications.md deleted file mode 100644 index 5f85dd04f3..0000000000 --- a/_archive/docs/0.3/docs/notifications.md +++ /dev/null @@ -1,1648 +0,0 @@ -# Notifications -[[toc]] - -## Introduction - -In addition to support for [sending email](/docs/mail), Hypervel provides support for sending notifications across a variety of delivery channels, including email, SMS (via [Vonage](https://www.vonage.com/communications-apis/), formerly known as Nexmo), and [Slack](https://slack.com). In addition, a variety of [community built notification channels](https://laravel-notification-channels.com/about/#suggesting-a-new-channel) have been created to send notifications over dozens of different channels! Notifications may also be stored in a database so they may be displayed in your web interface. - -Typically, notifications should be short, informational messages that notify users of something that occurred in your application. For example, if you are writing a billing application, you might send an "Invoice Paid" notification to your users via the email and SMS channels. - -## Generating Notifications - -In Hypervel, each notification is represented by a single class that is typically stored in the `app/Notifications` directory. Don't worry if you don't see this directory in your application - it will be created for you when you run the `make:notification` Artisan command: - -```shell:no-line-numbers -php artisan make:notification InvoicePaid -``` - -This command will place a fresh notification class in your `app/Notifications` directory. Each notification class contains a `via` method and a variable number of message building methods, such as `toMail` or `toDatabase`, that convert the notification to a message tailored for that particular channel. - -## Sending Notifications - -### Using the Notifiable Trait - -Notifications may be sent in two ways: using the `notify` method of the `Notifiable` trait or using the `Notification` [facade](/docs/facades). The `Notifiable` trait is included on your application's `App\Models\User` model by default: - -```php -notify(new InvoicePaid($invoice)); -``` - -::: note -Remember, you may use the `Notifiable` trait on any of your models. You are not limited to only including it on your `User` model. -::: - -### Using the Notification Facade - -Alternatively, you may send notifications via the `Notification` [facade](/docs/facades). This approach is useful when you need to send a notification to multiple notifiable entities such as a collection of users. To send notifications using the facade, pass all of the notifiable entities and the notification instance to the `send` method: - -```php -use Hypervel\Support\Facades\Notification; - -Notification::send($users, new InvoicePaid($invoice)); -``` - -You can also send notifications immediately using the `sendNow` method. This method will send the notification immediately even if the notification implements the `ShouldQueue` interface: - -```php -Notification::sendNow($developers, new DeploymentCompleted($deployment)); -``` - -### Specifying Delivery Channels - -Every notification class has a `via` method that determines on which channels the notification will be delivered. Notifications may be sent on the `mail`, `database`, `broadcast`, `vonage`, and `slack` channels. - -The `via` method receives a `$notifiable` instance, which will be an instance of the class to which the notification is being sent. You may use `$notifiable` to determine which channels the notification should be delivered on: - -```php -/** - * Get the notification's delivery channels. - * - * @return array - */ -public function via(object $notifiable): array -{ - return $notifiable->prefers_sms ? ['vonage'] : ['mail', 'database']; -} -``` - -### Queueing Notifications - -::: warning -Before queueing notifications you should configure your queue and [start a worker](/docs/queues#running-the-queue-worker). -::: - -Sending notifications can take time, especially if the channel needs to make an external API call to deliver the notification. To speed up your application's response time, let your notification be queued by adding the `ShouldQueue` interface and `Queueable` trait to your class. The interface and trait are already imported for all notifications generated using the `make:notification` command, so you may immediately add them to your notification class: - -```php -notify(new InvoicePaid($invoice)); -``` - -When queueing notifications, a queued job will be created for each recipient and channel combination. For example, six jobs will be dispatched to the queue if your notification has three recipients and two channels. - -#### Delaying Notifications - -If you would like to delay the delivery of the notification, you may chain the `delay` method onto your notification instantiation: - -```php -$delay = now()->addMinutes(10); - -$user->notify((new InvoicePaid($invoice))->delay($delay)); -``` - -You may pass an array to the `delay` method to specify the delay amount for specific channels: - -```php -$user->notify((new InvoicePaid($invoice))->delay([ - 'mail' => now()->addMinutes(5), - 'sms' => now()->addMinutes(10), -])); -``` - -Alternatively, you may define a `withDelay` method on the notification class itself. The `withDelay` method should return an array of channel names and delay values: - -```php -/** - * Determine the notification's delivery delay. - * - * @return array - */ -public function withDelay(object $notifiable): array -{ - return [ - 'mail' => now()->addMinutes(5), - 'sms' => now()->addMinutes(10), - ]; -} -``` - -#### Customizing the Notification Queue Connection - -By default, queued notifications will be queued using your application's default queue connection. If you would like to specify a different connection that should be used for a particular notification, you may call the `onConnection` method from your notification's constructor: - -```php -onConnection('redis'); - } -} -``` - -Or, if you would like to specify a specific queue connection that should be used for each notification channel supported by the notification, you may define a `viaConnections` method on your notification. This method should return an array of channel name / queue connection name pairs: - -```php -/** - * Determine which connections should be used for each notification channel. - * - * @return array - */ -public function viaConnections(): array -{ - return [ - 'mail' => 'redis', - 'database' => 'sync', - ]; -} -``` - -#### Customizing Notification Channel Queues - -If you would like to specify a specific queue that should be used for each notification channel supported by the notification, you may define a `viaQueues` method on your notification. This method should return an array of channel name / queue name pairs: - -```php -/** - * Determine which queues should be used for each notification channel. - * - * @return array - */ -public function viaQueues(): array -{ - return [ - 'mail' => 'mail-queue', - 'slack' => 'slack-queue', - ]; -} -``` - -#### Queued Notification Middleware - -Queued notifications may define middleware [just like queued jobs](/docs/queues#job-middleware). To get started, define a `middleware` method on your notification class. The `middleware` method will receive `$notifiable` and `$channel` variables, which allow you to customize the returned middleware based on the notification's destination: - -```php -use Hypervel\Queue\Middleware\RateLimited; - -/** - * Get the middleware the notification job should pass through. - * - * @return array - */ -public function middleware(object $notifiable, string $channel) -{ - return match ($channel) { - 'email' => [new RateLimited('postmark')], - 'slack' => [new RateLimited('slack')], - default => [], - }; -} -``` - -#### Queued Notifications and Database Transactions - -When queued notifications are dispatched within database transactions, they may be processed by the queue before the database transaction has committed. When this happens, any updates you have made to models or database records during the database transaction may not yet be reflected in the database. In addition, any models or database records created within the transaction may not exist in the database. If your notification depends on these models, unexpected errors can occur when the job that sends the queued notification is processed. - -If your queue connection's `after_commit` configuration option is set to `false`, you may still indicate that a particular queued notification should be dispatched after all open database transactions have been committed by calling the `afterCommit` method when sending the notification: - -```php -use App\Notifications\InvoicePaid; - -$user->notify((new InvoicePaid($invoice))->afterCommit()); -``` - -Alternatively, you may call the `afterCommit` method from your notification's constructor: - -```php -afterCommit(); - } -} -``` - -::: note -To learn more about working around these issues, please review the documentation regarding [queued jobs and database transactions](/docs/queues#jobs-and-database-transactions). -::: - -#### Determining if a Queued Notification Should Be Sent - -After a queued notification has been dispatched for the queue for background processing, it will typically be accepted by a queue worker and sent to its intended recipient. - -However, if you would like to make the final determination on whether the queued notification should be sent after it is being processed by a queue worker, you may define a `shouldSend` method on the notification class. If this method returns `false`, the notification will not be sent: - -```php -/** - * Determine if the notification should be sent. - */ -public function shouldSend(object $notifiable, string $channel): bool -{ - return $this->invoice->isPaid(); -} -``` - -### On-Demand Notifications - -Sometimes you may need to send a notification to someone who is not stored as a "user" of your application. Using the `Notification` facade's `route` method, you may specify ad-hoc notification routing information before sending the notification: - -```php -use Hypervel\Broadcasting\Channel; -use Hypervel\Support\Facades\Notification; - -Notification::route('mail', 'taylor@example.com') - ->route('vonage', '5555555555') - ->route('slack', '#slack-channel') - ->route('broadcast', [new Channel('channel-name')]) - ->notify(new InvoicePaid($invoice)); -``` - -If you would like to provide the recipient's name when sending an on-demand notification to the `mail` route, you may provide an array that contains the email address as the key and the name as the value of the first element in the array: - -```php -Notification::route('mail', [ - 'barrett@example.com' => 'Barrett Blair', -])->notify(new InvoicePaid($invoice)); -``` - -Using the `routes` method, you may provide ad-hoc routing information for multiple notification channels at once: - -```php -Notification::routes([ - 'mail' => ['barrett@example.com' => 'Barrett Blair'], - 'vonage' => '5555555555', -])->notify(new InvoicePaid($invoice)); -``` - -## Mail Notifications - -### Formatting Mail Messages - -If a notification supports being sent as an email, you should define a `toMail` method on the notification class. This method will receive a `$notifiable` entity and should return an `Hypervel\Notifications\Messages\MailMessage` instance. - -The `MailMessage` class contains a few simple methods to help you build transactional email messages. Mail messages may contain lines of text as well as a "call to action". Let's take a look at an example `toMail` method: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - $url = url('/invoice/'.$this->invoice->id); - - return (new MailMessage) - ->greeting('Hello!') - ->line('One of your invoices has been paid!') - ->lineIf($this->amount > 0, "Amount paid: {$this->amount}") - ->action('View Invoice', $url) - ->line('Thank you for using our application!'); -} -``` - -::: note -Note we are using `$this->invoice->id` in our `toMail` method. You may pass any data your notification needs to generate its message into the notification's constructor. -::: - -In this example, we register a greeting, a line of text, a call to action, and then another line of text. These methods provided by the `MailMessage` object make it simple and fast to format small transactional emails. The mail channel will then translate the message components into a beautiful, responsive HTML email template with a plain-text counterpart. Here is an example of an email generated by the `mail` channel: - -::: note -When sending mail notifications, be sure to set the `name` configuration option in your `config/app.php` configuration file. This value will be used in the header and footer of your mail notification messages. -::: - -#### Error Messages - -Some notifications inform users of errors, such as a failed invoice payment. You may indicate that a mail message is regarding an error by calling the `error` method when building your message. When using the `error` method on a mail message, the call to action button will be red instead of black: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage) - ->error() - ->subject('Invoice Payment Failed') - ->line('...'); -} -``` - -#### Other Mail Notification Formatting Options - -Instead of defining the "lines" of text in the notification class, you may use the `view` method to specify a custom template that should be used to render the notification email: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage)->view( - 'mail.invoice.paid', ['invoice' => $this->invoice] - ); -} -``` - -You may specify a plain-text view for the mail message by passing the view name as the second element of an array that is given to the `view` method: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage)->view( - ['mail.invoice.paid', 'mail.invoice.paid-text'], - ['invoice' => $this->invoice] - ); -} -``` - -Or, if your message only has a plain-text view, you may utilize the `text` method: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage)->text( - 'mail.invoice.paid-text', ['invoice' => $this->invoice] - ); -} -``` - -### Customizing the Sender - -By default, the email's sender / from address is defined in the `config/mail.php` configuration file. However, you may specify the from address for a specific notification using the `from` method: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage) - ->from('barrett@example.com', 'Barrett Blair') - ->line('...'); -} -``` - -### Customizing the Recipient - -When sending notifications via the `mail` channel, the notification system will automatically look for an `email` property on your notifiable entity. You may customize which email address is used to deliver the notification by defining a `routeNotificationForMail` method on the notifiable entity: - -```php -|string - */ - public function routeNotificationForMail(Notification $notification): array|string - { - // Return email address only... - return $this->email_address; - - // Return email address and name... - return [$this->email_address => $this->name]; - } -} -``` - -### Customizing the Subject - -By default, the email's subject is the class name of the notification formatted to "Title Case". So, if your notification class is named `InvoicePaid`, the email's subject will be `Invoice Paid`. If you would like to specify a different subject for the message, you may call the `subject` method when building your message: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage) - ->subject('Notification Subject') - ->line('...'); -} -``` - -### Customizing the Mailer - -By default, the email notification will be sent using the default mailer defined in the `config/mail.php` configuration file. However, you may specify a different mailer at runtime by calling the `mailer` method when building your message: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage) - ->mailer('postmark') - ->line('...'); -} -``` - -### Customizing the Templates - -You can modify the HTML and plain-text template used by mail notifications by publishing the notification package's resources. After running this command, the mail notification templates will be located in the `resources/views/vendor/notifications` directory: - -```shell:no-line-numbers -php artisan vendor:publish hypervel/notifications -``` - -### Attachments - -To add attachments to an email notification, use the `attach` method while building your message. The `attach` method accepts the absolute path to the file as its first argument: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage) - ->greeting('Hello!') - ->attach('/path/to/file'); -} -``` - -::: note -The `attach` method offered by notification mail messages also accepts [attachable objects](/docs/mail#attachable-objects). Please consult the comprehensive [attachable object documentation](/docs/mail#attachable-objects) to learn more. -::: - -When attaching files to a message, you may also specify the display name and / or MIME type by passing an `array` as the second argument to the `attach` method: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage) - ->greeting('Hello!') - ->attach('/path/to/file', [ - 'as' => 'name.pdf', - 'mime' => 'application/pdf', - ]); -} -``` - -Unlike attaching files in mailable objects, you may not attach a file directly from a storage disk using `attachFromStorage`. You should rather use the `attach` method with an absolute path to the file on the storage disk. Alternatively, you could return a [mailable](/docs/mail#generating-mailables) from the `toMail` method: - -```php -use App\Mail\InvoicePaid as InvoicePaidMailable; - -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): Mailable -{ - return (new InvoicePaidMailable($this->invoice)) - ->to($notifiable->email) - ->attachFromStorage('/path/to/file'); -} -``` - -When necessary, multiple files may be attached to a message using the `attachMany` method: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage) - ->greeting('Hello!') - ->attachMany([ - '/path/to/forge.svg', - '/path/to/vapor.svg' => [ - 'as' => 'Logo.svg', - 'mime' => 'image/svg+xml', - ], - ]); -} -``` - -#### Raw Data Attachments - -The `attachData` method may be used to attach a raw string of bytes as an attachment. When calling the `attachData` method, you should provide the filename that should be assigned to the attachment: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage) - ->greeting('Hello!') - ->attachData($this->pdf, 'name.pdf', [ - 'mime' => 'application/pdf', - ]); -} -``` - -### Adding Tags and Metadata - -Some third-party email providers such as Mailgun and Postmark support message "tags" and "metadata", which may be used to group and track emails sent by your application. You may add tags and metadata to an email message via the `tag` and `metadata` methods: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage) - ->greeting('Comment Upvoted!') - ->tag('upvote') - ->metadata('comment_id', $this->comment->id); -} -``` - -If your application is using the Mailgun driver, you may consult Mailgun's documentation for more information on [tags](https://documentation.mailgun.com/en/latest/user_manual.html#tagging-1) and [metadata](https://documentation.mailgun.com/en/latest/user_manual.html#attaching-data-to-messages). Likewise, the Postmark documentation may also be consulted for more information on their support for [tags](https://postmarkapp.com/blog/tags-support-for-smtp) and [metadata](https://postmarkapp.com/support/article/1125-custom-metadata-faq). - -If your application is using Amazon SES to send emails, you should use the `metadata` method to attach [SES "tags"](https://docs.aws.amazon.com/ses/latest/APIReference/API_MessageTag.html) to the message. - -### Customizing the Symfony Message - -The `withSymfonyMessage` method of the `MailMessage` class allows you to register a closure which will be invoked with the Symfony Message instance before sending the message. This gives you an opportunity to deeply customize the message before it is delivered: - -```php -use Symfony\Component\Mime\Email; - -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage) - ->withSymfonyMessage(function (Email $message) { - $message->getHeaders()->addTextHeader( - 'Custom-Header', 'Header Value' - ); - }); -} -``` - -### Using Mailables - -If needed, you may return a full [mailable object](/docs/mail) from your notification's `toMail` method. When returning a `Mailable` instead of a `MailMessage`, you will need to specify the message recipient using the mailable object's `to` method: - -```php -use App\Mail\InvoicePaid as InvoicePaidMailable; -use Hypervel\Mail\Mailable; - -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): Mailable -{ - return (new InvoicePaidMailable($this->invoice)) - ->to($notifiable->email); -} -``` - -#### Mailables and On-Demand Notifications - -If you are sending an [on-demand notification](#on-demand-notifications), the `$notifiable` instance given to the `toMail` method will be an instance of `Hypervel\Notifications\AnonymousNotifiable`, which offers a `routeNotificationFor` method that may be used to retrieve the email address the on-demand notification should be sent to: - -```php -use App\Mail\InvoicePaid as InvoicePaidMailable; -use Hypervel\Notifications\AnonymousNotifiable; -use Hypervel\Mail\Mailable; - -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): Mailable -{ - $address = $notifiable instanceof AnonymousNotifiable - ? $notifiable->routeNotificationFor('mail') - : $notifiable->email; - - return (new InvoicePaidMailable($this->invoice)) - ->to($address); -} -``` - -### Previewing Mail Notifications - -When designing a mail notification template, it is convenient to quickly preview the rendered mail message in your browser like a typical Blade template. For this reason, Hypervel allows you to return any mail message generated by a mail notification directly from a route closure or controller. When a `MailMessage` is returned, it will be rendered and displayed in the browser, allowing you to quickly preview its design without needing to send it to an actual email address: - -```php -use App\Models\Invoice; -use App\Notifications\InvoicePaid; - -Route::get('/notification', function () { - $invoice = Invoice::find(1); - - return (new InvoicePaid($invoice)) - ->toMail($invoice->user); -}); -``` - -## Markdown Mail Notifications - -Markdown mail notifications allow you to take advantage of the pre-built templates of mail notifications, while giving you more freedom to write longer, customized messages. Since the messages are written in Markdown, Hypervel is able to render beautiful, responsive HTML templates for the messages while also automatically generating a plain-text counterpart. - -### Generating the Message - -To generate a notification with a corresponding Markdown template, you may use the `--markdown` option of the `make:notification` Artisan command: - -```shell:no-line-numbers -php artisan make:notification InvoicePaid --markdown=mail.invoice.paid -``` - -Like all other mail notifications, notifications that use Markdown templates should define a `toMail` method on their notification class. However, instead of using the `line` and `action` methods to construct the notification, use the `markdown` method to specify the name of the Markdown template that should be used. An array of data you wish to make available to the template may be passed as the method's second argument: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - $url = url('/invoice/'.$this->invoice->id); - - return (new MailMessage) - ->subject('Invoice Paid') - ->markdown('mail.invoice.paid', ['url' => $url]); -} -``` - -### Writing the Message - -Markdown mail notifications use a combination of Blade components and Markdown syntax which allow you to easily construct notifications while leveraging Hypervel's pre-crafted notification components: - -```html - -# Invoice Paid - -Your invoice has been paid! - - -View Invoice - - -Thanks,
      -{{ config('app.name') }} -
      -``` - -::: note -Do not use excess indentation when writing Markdown emails. Per Markdown standards, Markdown parsers will render indented content as code blocks. -::: - -#### Button Component - -The button component renders a centered button link. The component accepts two arguments, a `url` and an optional `color`. Supported colors are `primary`, `green`, and `red`. You may add as many button components to a notification as you wish: - -```html - -View Invoice - -``` - -#### Panel Component - -The panel component renders the given block of text in a panel that has a slightly different background color than the rest of the notification. This allows you to draw attention to a given block of text: - -```html - -This is the panel content. - -``` - -#### Table Component - -The table component allows you to transform a Markdown table into an HTML table. The component accepts the Markdown table as its content. Table column alignment is supported using the default Markdown table alignment syntax: - -```html - -| Hypervel| Table | Example | -| ------------- | :-----------: | ------------: | -| Col 2 is | Centered | $10 | -| Col 3 is | Right-Aligned | $20 | - -``` - -### Customizing the Components - -You may export all of the Markdown notification components to your own application for customization. To export the components, use the `vendor:publish` Artisan command to publish the mail package. - -```shell:no-line-numbers -php artisan vendor:publish hypervel/mail -``` - -This command will publish the Markdown mail components to the `resources/views/vendor/mail` directory. The `mail` directory will contain an `html` and a `text` directory, each containing their respective representations of every available component. You are free to customize these components however you like. - -#### Customizing the CSS - -After exporting the components, the `resources/views/vendor/mail/html/themes` directory will contain a `default.css` file. You may customize the CSS in this file and your styles will automatically be in-lined within the HTML representations of your Markdown notifications. - -If you would like to build an entirely new theme for Hypervel's Markdown components, you may place a CSS file within the `html/themes` directory. After naming and saving your CSS file, update the `theme` option of the `mail` configuration file to match the name of your new theme. - -To customize the theme for an individual notification, you may call the `theme` method while building the notification's mail message. The `theme` method accepts the name of the theme that should be used when sending the notification: - -```php -/** - * Get the mail representation of the notification. - */ -public function toMail(object $notifiable): MailMessage -{ - return (new MailMessage) - ->theme('invoice') - ->subject('Invoice Paid') - ->markdown('mail.invoice.paid', ['url' => $url]); -} -``` - -## Database Notifications - -### Prerequisites - -The `database` notification channel stores the notification information in a database table. This table will contain information such as the notification type as well as a JSON data structure that describes the notification. - -You can query the table to display the notifications in your application's user interface. But, before you can do that, you will need to create a database table to hold your notifications. You may use the `make:notifications-table` command to generate a [migration](/docs/migrations) with the proper table schema: - -```shell:no-line-numbers -php artisan make:notifications-table - -php artisan migrate -``` - -::: note -If your notifiable models are using [UUID or ULID primary keys](/docs/eloquent#uuid-and-ulid-keys), you should replace the `morphs` method with [`uuidMorphs`](/docs/migrations#column-method-uuidMorphs) or [`ulidMorphs`](/docs/migrations#column-method-ulidMorphs) in the notification table migration. -::: - -### Formatting Database Notifications - -If a notification supports being stored in a database table, you should define a `toDatabase` or `toArray` method on the notification class. This method will receive a `$notifiable` entity and should return a plain PHP array. The returned array will be encoded as JSON and stored in the `data` column of your `notifications` table. Let's take a look at an example `toArray` method: - -```php -/** - * Get the array representation of the notification. - * - * @return array - */ -public function toArray(object $notifiable): array -{ - return [ - 'invoice_id' => $this->invoice->id, - 'amount' => $this->invoice->amount, - ]; -} -``` - -When a notification is stored in your application's database, the `type` column will be set to the notification's class name by default, and the `read_at` column will be `null`. However, you can customize this behavior by defining the `databaseType` and `initialDatabaseReadAtValue` methods in your notification class: - -```php -use Hypervel\Support\Carbon; - -/** - * Get the notification's database type. - */ -public function databaseType(object $notifiable): string -{ - return 'invoice-paid'; -} - -/** - * Get the initial value for the "read_at" column. - */ -public function initialDatabaseReadAtValue(): ?Carbon -{ - return null; -} -``` - -#### `toDatabase` vs. `toArray` - -The `toArray` method is also used by the `broadcast` channel to determine which data to broadcast to your JavaScript powered frontend. If you would like to have two different array representations for the `database` and `broadcast` channels, you should define a `toDatabase` method instead of a `toArray` method. - -### Accessing the Notifications - -Once notifications are stored in the database, you need a convenient way to access them from your notifiable entities. The `Hypervel\Notifications\Notifiable` trait, which is included on Hypervel's default `App\Models\User` model, includes a `notifications` [Eloquent relationship](/docs/eloquent-relationships) that returns the notifications for the entity. To fetch notifications, you may access this method like any other Eloquent relationship. By default, notifications will be sorted by the `created_at` timestamp with the most recent notifications at the beginning of the collection: - -```php -$user = App\Models\User::find(1); - -foreach ($user->notifications as $notification) { - echo $notification->type; -} -``` - -If you want to retrieve only the "unread" notifications, you may use the `unreadNotifications` relationship. Again, these notifications will be sorted by the `created_at` timestamp with the most recent notifications at the beginning of the collection: - -```php -$user = App\Models\User::find(1); - -foreach ($user->unreadNotifications as $notification) { - echo $notification->type; -} -``` - -::: note -To access your notifications from your JavaScript client, you should define a notification controller for your application which returns the notifications for a notifiable entity, such as the current user. You may then make an HTTP request to that controller's URL from your JavaScript client. -::: - -### Marking Notifications as Read - -Typically, you will want to mark a notification as "read" when a user views it. The `Hypervel\Notifications\Notifiable` trait provides a `markAsRead` method, which updates the `read_at` column on the notification's database record: - -```php -$user = App\Models\User::find(1); - -foreach ($user->unreadNotifications as $notification) { - $notification->markAsRead(); -} -``` - -However, instead of looping through each notification, you may use the `markAsRead` method directly on a collection of notifications: - -```php -$user->unreadNotifications->markAsRead(); -``` - -You may also use a mass-update query to mark all of the notifications as read without retrieving them from the database: - -```php -$user = App\Models\User::find(1); - -$user->unreadNotifications()->update(['read_at' => now()]); -``` - -You may `delete` the notifications to remove them from the table entirely: - -```php -$user->notifications()->delete(); -``` - -## Broadcast Notifications - -### Prerequisites - -Before broadcasting notifications, you should configure and be familiar with Hypervel's [event broadcasting](/docs/broadcasting) services. Event broadcasting provides a way to react to server-side Hypervel events from your JavaScript powered frontend. - -### Formatting Broadcast Notifications - -The `broadcast` channel broadcasts notifications using Hypervel's [event broadcasting](/docs/broadcasting) services, allowing your JavaScript powered frontend to catch notifications in realtime. If a notification supports broadcasting, you can define a `toBroadcast` method on the notification class. This method will receive a `$notifiable` entity and should return a `BroadcastMessage` instance. If the `toBroadcast` method does not exist, the `toArray` method will be used to gather the data that should be broadcast. The returned data will be encoded as JSON and broadcast to your JavaScript powered frontend. Let's take a look at an example `toBroadcast` method: - -```php -use Hypervel\Notifications\Messages\BroadcastMessage; - -/** - * Get the broadcastable representation of the notification. - */ -public function toBroadcast(object $notifiable): BroadcastMessage -{ - return new BroadcastMessage([ - 'invoice_id' => $this->invoice->id, - 'amount' => $this->invoice->amount, - ]); -} -``` - -#### Broadcast Queue Configuration - -All broadcast notifications are queued for broadcasting. If you would like to configure the queue connection or queue name that is used to queue the broadcast operation, you may use the `onConnection` and `onQueue` methods of the `BroadcastMessage`: - -```php -return (new BroadcastMessage($data)) - ->onConnection('sqs') - ->onQueue('broadcasts'); -``` - -#### Customizing the Notification Type - -In addition to the data you specify, all broadcast notifications also have a `type` field containing the full class name of the notification. If you would like to customize the notification `type`, you may define a `broadcastType` method on the notification class: - -```php -/** - * Get the type of the notification being broadcast. - */ -public function broadcastType(): string -{ - return 'broadcast.message'; -} -``` - -### Listening for Notifications - -Notifications will broadcast on a private channel formatted using a `{notifiable}.{id}` convention. So, if you are sending a notification to an `App\Models\User` instance with an ID of `1`, the notification will be broadcast on the `App.Models.User.1` private channel. When using [Laravel Echo](/docs/broadcasting#client-side-installation), you may easily listen for notifications on a channel using the `notification` method: - -```js -Echo.private('App.Models.User.' + userId) - .notification((notification) => { - console.log(notification.type); - }); -``` - -#### Customizing the Notification Channel - -If you would like to customize which channel that an entity's broadcast notifications are broadcast on, you may define a `receivesBroadcastNotificationsOn` method on the notifiable entity: - -```php -id; - } -} -``` - -## SMS Notifications - -### Prerequisites - -Sending SMS notifications in Hypervel is powered by [Vonage](https://www.vonage.com/) (formerly known as Nexmo). Before you can send notifications via Vonage, you need to install the `laravel/vonage-notification-channel` and `guzzlehttp/guzzle` packages: - -```shell:no-line-numbers -composer require laravel/vonage-notification-channel guzzlehttp/guzzle -``` - -The package includes a [configuration file](https://github.com/laravel/vonage-notification-channel/blob/3.x/config/vonage.php). However, you are not required to export this configuration file to your own application. You can simply use the `VONAGE_KEY` and `VONAGE_SECRET` environment variables to define your Vonage public and secret keys. - -After defining your keys, you should set a `VONAGE_SMS_FROM` environment variable that defines the phone number that your SMS messages should be sent from by default. You may generate this phone number within the Vonage control panel: - -```ini -VONAGE_SMS_FROM=15556666666 -``` - -### Formatting SMS Notifications - -If a notification supports being sent as an SMS, you should define a `toVonage` method on the notification class. This method will receive a `$notifiable` entity and should return an `Hypervel\Notifications\Messages\VonageMessage` instance: - -```php -use Hypervel\Notifications\Messages\VonageMessage; - -/** - * Get the Vonage / SMS representation of the notification. - */ -public function toVonage(object $notifiable): VonageMessage -{ - return (new VonageMessage) - ->content('Your SMS message content'); -} -``` - -#### Unicode Content - -If your SMS message will contain unicode characters, you should call the `unicode` method when constructing the `VonageMessage` instance: - -```php -use Hypervel\Notifications\Messages\VonageMessage; - -/** - * Get the Vonage / SMS representation of the notification. - */ -public function toVonage(object $notifiable): VonageMessage -{ - return (new VonageMessage) - ->content('Your unicode message') - ->unicode(); -} -``` - -### Customizing the "From" Number - -If you would like to send some notifications from a phone number that is different from the phone number specified by your `VONAGE_SMS_FROM` environment variable, you may call the `from` method on a `VonageMessage` instance: - -```php -use Hypervel\Notifications\Messages\VonageMessage; - -/** - * Get the Vonage / SMS representation of the notification. - */ -public function toVonage(object $notifiable): VonageMessage -{ - return (new VonageMessage) - ->content('Your SMS message content') - ->from('15554443333'); -} -``` - -### Adding a Client Reference - -If you would like to keep track of costs per user, team, or client, you may add a "client reference" to the notification. Vonage will allow you to generate reports using this client reference so that you can better understand a particular customer's SMS usage. The client reference can be any string up to 40 characters: - -```php -use Hypervel\Notifications\Messages\VonageMessage; - -/** - * Get the Vonage / SMS representation of the notification. - */ -public function toVonage(object $notifiable): VonageMessage -{ - return (new VonageMessage) - ->clientReference((string) $notifiable->id) - ->content('Your SMS message content'); -} -``` - -### Routing SMS Notifications - -To route Vonage notifications to the proper phone number, define a `routeNotificationForVonage` method on your notifiable entity: - -```php -phone_number; - } -} -``` - -## Slack Notifications - -### Prerequisites - -Before sending Slack notifications, you must create a [Slack App](https://api.slack.com/apps?new_app=1) for your Slack workspace. - -If you only need to send notifications to the same Slack workspace that the App is created in, you should ensure that your App has the `chat:write`, `chat:write.public`, and `chat:write.customize` scopes. If you want to send messages as your Slack App, you should ensure that your App also has the `chat:write:bot` scope. These scopes can be added from the "OAuth & Permissions" App management tab within Slack. - -Next, copy the App's "Bot User OAuth Token" and place it within a `slack` configuration array in your application's `services.php` configuration file. This token can be found on the "OAuth & Permissions" tab within Slack: - -```php -'slack' => [ - 'notifications' => [ - 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), - 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), - ], -], -``` - -#### App Distribution - -If your application will be sending notifications to external Slack workspaces that are owned by your application's users, you will need to "distribute" your App via Slack. App distribution can be managed from your App's "Manage Distribution" tab within Slack. - -### Formatting Slack Notifications - -If a notification supports being sent as a Slack message, you should define a `toSlack` method on the notification class. This method will receive a `$notifiable` entity and should return an `Hypervel\Notifications\Slack\SlackMessage` instance. You can construct rich notifications using [Slack's Block Kit API](https://api.slack.com/block-kit). The following example may be previewed in [Slack's Block Kit builder](https://app.slack.com/block-kit-builder/T01KWS6K23Z#%7B%22blocks%22:%5B%7B%22type%22:%22header%22,%22text%22:%7B%22type%22:%22plain_text%22,%22text%22:%22Invoice%20Paid%22%7D%7D,%7B%22type%22:%22context%22,%22elements%22:%5B%7B%22type%22:%22plain_text%22,%22text%22:%22Customer%20%231234%22%7D%5D%7D,%7B%22type%22:%22section%22,%22text%22:%7B%22type%22:%22plain_text%22,%22text%22:%22An%20invoice%20has%20been%20paid.%22%7D,%22fields%22:%5B%7B%22type%22:%22mrkdwn%22,%22text%22:%22*Invoice%20No:*%5Cn1000%22%7D,%7B%22type%22:%22mrkdwn%22,%22text%22:%22*Invoice%20Recipient:*%5Cntaylor@laravel.com%22%7D%5D%7D,%7B%22type%22:%22divider%22%7D,%7B%22type%22:%22section%22,%22text%22:%7B%22type%22:%22plain_text%22,%22text%22:%22Congratulations!%22%7D%7D%5D%7D): - -```php -use Hypervel\Notifications\Slack\BlockKit\Blocks\ContextBlock; -use Hypervel\Notifications\Slack\BlockKit\Blocks\SectionBlock; -use Hypervel\Notifications\Slack\BlockKit\Composites\ConfirmObject; -use Hypervel\Notifications\Slack\SlackMessage; - -/** - * Get the Slack representation of the notification. - */ -public function toSlack(object $notifiable): SlackMessage -{ - return (new SlackMessage) - ->text('One of your invoices has been paid!') - ->headerBlock('Invoice Paid') - ->contextBlock(function (ContextBlock $block) { - $block->text('Customer #1234'); - }) - ->sectionBlock(function (SectionBlock $block) { - $block->text('An invoice has been paid.'); - $block->field("*Invoice No:*\n1000")->markdown(); - $block->field("*Invoice Recipient:*\ntaylor@hypervel.org")->markdown(); - }) - ->dividerBlock() - ->sectionBlock(function (SectionBlock $block) { - $block->text('Congratulations!'); - }); -} -``` - -#### Using Slack's Block Kit Builder Template - -Instead of using the fluent message builder methods to construct your Block Kit message, you may provide the raw JSON payload generated by Slack's Block Kit Builder to the `usingBlockKitTemplate` method: - -```php -use Hypervel\Notifications\Slack\SlackMessage; -use Hypervel\Support\Str; - -/** - * Get the Slack representation of the notification. - */ -public function toSlack(object $notifiable): SlackMessage -{ - $template = <<usingBlockKitTemplate($template); -} -``` - -### Slack Interactivity - -Slack's Block Kit notification system provides powerful features to [handle user interaction](https://api.slack.com/interactivity/handling). To utilize these features, your Slack App should have "Interactivity" enabled and a "Request URL" configured that points to a URL served by your application. These settings can be managed from the "Interactivity & Shortcuts" App management tab within Slack. - -In the following example, which utilizes the `actionsBlock` method, Slack will send a `POST` request to your "Request URL" with a payload containing the Slack user who clicked the button, the ID of the clicked button, and more. Your application can then determine the action to take based on the payload. You should also [verify the request](https://api.slack.com/authentication/verifying-requests-from-slack) was made by Slack: - -```php -use Hypervel\Notifications\Slack\BlockKit\Blocks\ActionsBlock; -use Hypervel\Notifications\Slack\BlockKit\Blocks\ContextBlock; -use Hypervel\Notifications\Slack\BlockKit\Blocks\SectionBlock; -use Hypervel\Notifications\Slack\SlackMessage; - -/** - * Get the Slack representation of the notification. - */ -public function toSlack(object $notifiable): SlackMessage -{ - return (new SlackMessage) - ->text('One of your invoices has been paid!') - ->headerBlock('Invoice Paid') - ->contextBlock(function (ContextBlock $block) { - $block->text('Customer #1234'); - }) - ->sectionBlock(function (SectionBlock $block) { - $block->text('An invoice has been paid.'); - }) - ->actionsBlock(function (ActionsBlock $block) { - // ID defaults to "button_acknowledge_invoice"... - $block->button('Acknowledge Invoice')->primary(); - - // Manually configure the ID... - $block->button('Deny')->danger()->id('deny_invoice'); - }); -} -``` - -#### Confirmation Modals - -If you would like users to be required to confirm an action before it is performed, you may invoke the `confirm` method when defining your button. The `confirm` method accepts a message and a closure which receives a `ConfirmObject` instance: - -```php -use Hypervel\Notifications\Slack\BlockKit\Blocks\ActionsBlock; -use Hypervel\Notifications\Slack\BlockKit\Blocks\ContextBlock; -use Hypervel\Notifications\Slack\BlockKit\Blocks\SectionBlock; -use Hypervel\Notifications\Slack\BlockKit\Composites\ConfirmObject; -use Hypervel\Notifications\Slack\SlackMessage; - -/** - * Get the Slack representation of the notification. - */ -public function toSlack(object $notifiable): SlackMessage -{ - return (new SlackMessage) - ->text('One of your invoices has been paid!') - ->headerBlock('Invoice Paid') - ->contextBlock(function (ContextBlock $block) { - $block->text('Customer #1234'); - }) - ->sectionBlock(function (SectionBlock $block) { - $block->text('An invoice has been paid.'); - }) - ->actionsBlock(function (ActionsBlock $block) { - $block->button('Acknowledge Invoice') - ->primary() - ->confirm( - 'Acknowledge the payment and send a thank you email?', - function (ConfirmObject $dialog) { - $dialog->confirm('Yes'); - $dialog->deny('No'); - } - ); - }); -} -``` - -#### Inspecting Slack Blocks - -If you would like to quickly inspect the blocks you've been building, you can invoke the `dd` method on the `SlackMessage` instance. The `dd` method will generate and dump a URL to Slack's [Block Kit Builder](https://app.slack.com/block-kit-builder/), which displays a preview of the payload and notification in your browser. You may pass `true` to the `dd` method to dump the raw payload: - -```php -return (new SlackMessage) - ->text('One of your invoices has been paid!') - ->headerBlock('Invoice Paid') - ->dd(); -``` - -### Routing Slack Notifications - -To direct Slack notifications to the appropriate Slack team and channel, define a `routeNotificationForSlack` method on your notifiable model. This method can return one of three values: - -- `null` - which defers routing to the channel configured in the notification itself. You may use the `to` method when building your `SlackMessage` to configure the channel within the notification. -- A string specifying the Slack channel to send the notification to, e.g. `#support-channel`. -- A `SlackRoute` instance, which allows you to specify an OAuth token and channel name, e.g. `SlackRoute::make($this->slack_channel, $this->slack_token)`. This method should be used to send notifications to external workspaces. - -For instance, returning `#support-channel` from the `routeNotificationForSlack` method will send the notification to the `#support-channel` channel in the workspace associated with the Bot User OAuth token located in your application's `services.php` configuration file: - -```php -slack_channel, $this->slack_token); - } -} -``` - -## Localizing Notifications - -Hypervel allows you to send notifications in a locale other than the HTTP request's current locale, and will even remember this locale if the notification is queued. - -To accomplish this, the `Hypervel\Notifications\Notification` class offers a `locale` method to set the desired language. The application will change into this locale when the notification is being evaluated and then revert back to the previous locale when evaluation is complete: - -```php -$user->notify((new InvoicePaid($invoice))->locale('es')); -``` - -Localization of multiple notifiable entries may also be achieved via the `Notification` facade: - -```php -Notification::locale('es')->send( - $users, new InvoicePaid($invoice) -); -``` - -### User Preferred Locales - -Sometimes, applications store each user's preferred locale. By implementing the `HasLocalePreference` contract on your notifiable model, you may instruct Hypervel to use this stored locale when sending a notification: - -```php -use Hypervel\Translation\Contracts\HasLocalePreference; - -class User extends Model implements HasLocalePreference -{ - /** - * Get the user's preferred locale. - */ - public function preferredLocale(): string - { - return $this->locale; - } -} -``` - -Once you have implemented the interface, Hypervel will automatically use the preferred locale when sending notifications and mailables to the model. Therefore, there is no need to call the `locale` method when using this interface: - -```php -$user->notify(new InvoicePaid($invoice)); -``` - -## Testing - -You may use the `Notification` facade's `fake` method to prevent notifications from being sent. Typically, sending notifications is unrelated to the code you are actually testing. Most likely, it is sufficient to simply assert that Hypervel was instructed to send a given notification. - -After calling the `Notification` facade's `fake` method, you may then assert that notifications were instructed to be sent to users and even inspect the data the notifications received: - -```php -order->id === $order->id; - } -); -``` - -#### On-Demand Notifications - -If the code you are testing sends [on-demand notifications](#on-demand-notifications), you can test that the on-demand notification was sent via the `assertSentOnDemand` method: - -```php -Notification::assertSentOnDemand(OrderShipped::class); -``` - -By passing a closure as the second argument to the `assertSentOnDemand` method, you may determine if an on-demand notification was sent to the correct "route" address: - -```php -Notification::assertSentOnDemand( - OrderShipped::class, - function (OrderShipped $notification, array $channels, object $notifiable) use ($user) { - return $notifiable->routes['mail'] === $user->email; - } -); -``` - -## Notification Events - -#### Notification Sending Event - -When a notification is sending, the `Hypervel\Notifications\Events\NotificationSending` event is dispatched by the notification system. This contains the "notifiable" entity and the notification instance itself. You may create [event listeners](/docs/events) for this event within your application: - -```php -use Hypervel\Notifications\Events\NotificationSending; - -class CheckNotificationStatus -{ - /** - * Handle the given event. - */ - public function handle(NotificationSending $event): void - { - // ... - } -} -``` - -The notification will not be sent if an event listener for the `NotificationSending` event returns `false` from its `handle` method: - -```php -/** - * Handle the given event. - */ -public function handle(NotificationSending $event): bool -{ - return false; -} -``` - -Within an event listener, you may access the `notifiable`, `notification`, and `channel` properties on the event to learn more about the notification recipient or the notification itself: - -```php -/** - * Handle the given event. - */ -public function handle(NotificationSending $event): void -{ - // $event->channel - // $event->notifiable - // $event->notification -} -``` - -#### Notification Sent Event - -When a notification is sent, the `Hypervel\Notifications\Events\NotificationSent` [event](/docs/events) is dispatched by the notification system. This contains the "notifiable" entity and the notification instance itself. You may create [event listeners](/docs/events) for this event within your application: - -```php -use Hypervel\Notifications\Events\NotificationSent; - -class LogNotification -{ - /** - * Handle the given event. - */ - public function handle(NotificationSent $event): void - { - // ... - } -} -``` - -Within an event listener, you may access the `notifiable`, `notification`, `channel`, and `response` properties on the event to learn more about the notification recipient or the notification itself: - -```php -/** - * Handle the given event. - */ -public function handle(NotificationSent $event): void -{ - // $event->channel - // $event->notifiable - // $event->notification - // $event->response -} -``` - -## Custom Channels - -Hypervel ships with a handful of notification channels, but you may want to write your own drivers to deliver notifications via other channels. Hypervel makes it simple. To get started, define a class that contains a `send` method. The method should receive two arguments: a `$notifiable` and a `$notification`. - -Within the `send` method, you may call methods on the notification to retrieve a message object understood by your channel and then send the notification to the `$notifiable` instance however you wish: - -```php -toVoice($notifiable); - - // Send notification to the $notifiable instance... - } -} -``` - -Once your notification channel class has been defined, you may return the class name from the `via` method of any of your notifications. In this example, the `toVoice` method of your notification can return whatever object you choose to represent voice messages. For example, you might define your own `VoiceMessage` class to represent these messages: - -```php -loggedOut) { - return; - } - - // If we've already retrieved the user for the current request we can just - // return it back immediately. We do not want to fetch the user data on - // every call to this method because that would be tremendously slow. - if (! is_null($this->user)) { - return $this->user; - } - - // ... - } -} -``` - -All guards are managed in `AuthManager`, which is registered as a singleton in the app container: - -```php -class AuthManager implements FactoryContract -{ - // ... - - /** - * The array of created "drivers". - * - * @var array - */ - protected $guards = []; - - // ... - - /** - * Attempt to get the guard from the local cache. - * - * @param string|null $name - * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard - */ - public function guard($name = null) - { - $name = $name ?: $this->getDefaultDriver(); - - return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name); - } -} -``` - -As you can see, the guard is resolved only once and kept in the `guards` property within the `AuthManager`. The `user` property in `SessionGuard` is cached for performance. This approach works perfectly in a traditional stateless PHP environment such as PHP-FPM. - -In order to work in long-living environments like Octane, Octane must flush these states before or after every request, for example: - -```php -class FlushAuthenticationState -{ - /** - * Handle the event. - * - * @param mixed $event - */ - public function handle($event): void - { - if ($event->sandbox->resolved('auth.driver')) { - $event->sandbox->forgetInstance('auth.driver'); - } - - if ($event->sandbox->resolved('auth')) { - with($event->sandbox->make('auth'), function ($auth) use ($event) { - $auth->setApplication($event->sandbox); - $auth->forgetGuards(); - }); - } - } -} -``` - -However, this mechanism is **far from sufficient** in coroutine environments. All singleton objects must be designed to serve multiple requests concurrently, which means their state needs to be isolated within a **Coroutine-Level Container (Context)**. - -This is the main reason why Laravel packages cannot run on Hypervel: state leakage between coroutines becomes a serious issue. - -## How To Port a Laravel Package? - -To port a Laravel package to Hypervel, you need to follow these steps: - -1. Replace Dependencies with Hypervel Equivalents -2. Replace PHPDocs with Native Type Declarations -3. Isolate States for Coroutines -4. Migrate from Service Provider to Config Provider -5. Adjust Tests for the Package - -The following sections demonstrate how to port Laravel's `Translation` package, step by step. - -### Replace Dependencies with Hypervel - -Laravel packages typically rely on `Illuminate`-namespaced components, such as contracts, app, support, filesystem, config, cache, etc. - -Laravel collects all package contracts in a standalone `Contracts` package, but Hypervel does **not**. Instead, Hypervel keeps such contracts within their respective packages. Therefore, you'll need to move all contracts to `Hypervel\Translation\Contracts`, for example: - -* `Illuminate\Contracts\Translation\HasLocalePreference::class` -* `Illuminate\Contracts\Translation\Loader::class` -* `Illuminate\Contracts\Translation\Translator::class` - -```php -connection()`) | -| Illuminate\Contracts\Debug\ExceptionHandler | Hypervel\Foundation\Exceptions\Handler | -| Illuminate\Contracts\Encryption\Encrypter | Hypervel\Encryption\Contracts\Encrypter | -| Illuminate\Contracts\Events\Dispatcher | Psr\EventDispatcher\EventDispatcherInterface, Hypervel\Event\Contracts\Dispatcher | -| Illuminate\Contracts\Filesystem\Cloud | Hypervel\Filesystem\Contracts\Cloud | -| Illuminate\Contracts\Filesystem\Factory | Hypervel\Filesystem\Contracts\Factory | -| Illuminate\Contracts\Filesystem\Filesystem | Hypervel\Filesystem\Contracts\Filesystem | -| Illuminate\Contracts\Foundation\Application | Psr\Container\ContainerInterface, Hypervel\Container\Contracts\Container | -| Illuminate\Contracts\Hashing\Hasher | Hypervel\Hashing\Contracts\Hasher | -| Illuminate\Http\Request | Psr\Http\Message\ServerRequestInterface, Hypervel\Http\Contracts\RequestContract | -| Illuminate\Http\Response | Hyperf\HttpServer\Contract\ResponseInterface, Hypervel\Http\Contracts\ResponseContract | -| Illuminate\Log\LogManager | Psr\Log\LoggerInterface | -| Illuminate\Contracts\Mail\Mailable | Hypervel\Mail\Contracts\Mailable | -| Illuminate\Contracts\Mail\Mailer | Hypervel\Mail\Contracts\Mailer | -| Illuminate\Contracts\Mail\MailQueue | Hypervel\Mail\Contracts\MailQueue | -| Illuminate\Contracts\Notifications\Dispatcher | Hypervel\Mail\Contracts\Mailable | -| Illuminate\Contracts\Notifications\Dispatcher | Hypervel\Notifications\Contracts\Dispatcher | -| Illuminate\Contracts\Notifications\Factory | Hypervel\Notifications\Contracts\Factory | -| Illuminate\Contracts\Notifications\Dispatcher | Hypervel\Notifications\Contracts\Dispatcher | -| Illuminate\Contracts\Pipeline\Pipeline | Hypervel\Support\Pipeline::make() | -| Illuminate\Contracts\Queue\EntityResolver | Hypervel\Queue\Contracts\EntityResolver | -| Illuminate\Contracts\Queue\Factory | Hypervel\Queue\Contracts\Factory | -| Illuminate\Contracts\Queue\Job | Hypervel\Queue\Contracts\Job | -| Illuminate\Contracts\Queue\Monitor | Hypervel\Queue\Contracts\Monitor | -| Illuminate\Contracts\Queue\Queue | Hypervel\Queue\Contracts\Queue | -| Illuminate\Contracts\Queue\QueueableCollection | Hypervel\Queue\Contracts\QueueableCollection | -| Illuminate\Contracts\Queue\QueueableEntity | Hypervel\Queue\Contracts\QueueableEntity | -| Illuminate\Contracts\Queue\ShouldQueue | Hypervel\Queue\Contracts\ShouldQueue | -| Illuminate\Contracts\Redis\Factory | Hyperf\Redis\RedisFactory (`getConnection()`) | -| Illuminate\Contracts\Routing\UrlGenerator | Hypervel\Router\Contracts\UrlGenerator | -| Illuminate\Contracts\Routing\UrlRoutable | Hypervel\Router\Contracts\UrlRoutable | -| Illuminate\Contracts\Session\Session | Hypervel\Session\Contracts\Session | -| Illuminate\Contracts\Support\Arrayable | Hypervel\Support\Contracts\Arrayable | -| Illuminate\Contracts\Support\Htmlable | Hypervel\Support\Contracts\Htmlable | -| Illuminate\Contracts\Support\Jsonable | Hypervel\Support\Contracts\Jsonable | -| Illuminate\Contracts\Support\MessageBag | Hypervel\Support\Contracts\MessageBag | -| Illuminate\Contracts\Support\MessageProvider | Hypervel\Support\Contracts\MessageProvider | -| Illuminate\Contracts\Support\Renderable | Hypervel\Support\Contracts\Renderable | -| Illuminate\Contracts\Support\Responsable | Hypervel\Support\Contracts\Responsable | -| Illuminate\Filesystem\Filesystem | Hypervel\Filesystem\Filesystem | -| Illuminate\Support\Arr | Hypervel\Support\Arr | -| Illuminate\Support\Carbon | Hypervel\Support\Carbon | -| Illuminate\Support\Collection | Hypervel\Support\Collection | -| Illuminate\Support\DateFactory | Hypervel\Support\DateFactory | -| Illuminate\Support\Fluent | Hypervel\Support\Fluent | -| Illuminate\Support\Manager | Hypervel\Support\Manager | -| Illuminate\Support\NamespacedItemResolver | Hypervel\Support\NamespacedItemResolver | -| Illuminate\Support\Number | Hypervel\Support\Number | -| Illuminate\Support\ServiceProvider | Hypervel\Support\ServiceProvider | -| Illuminate\Support\Sleep | Hypervel\Support\ServiceProvider | -| Illuminate\Support\Str | Hypervel\Support\Str | -| Illuminate\Support\Traits\Conditionable | Hypervel\Support\Traits\Conditionable | -| Illuminate\Support\Traits\Macroable | Hypervel\Support\Traits\Macroable | -| Illuminate\Support\Traits\ReflectsClosures | Hypervel\Support\Traits\ReflectsClosures | -| Illuminate\Contracts\Translation\Loader | Hypervel\Translation\Contracts\Loader | -| Illuminate\Contracts\Translation\Translator | Hypervel\Translation\Contracts\Translator | -| Illuminate\Contracts\Validation\Factory | Hypervel\Validation\Contracts\Factory | -| Illuminate\Contracts\Validation\ValidatesWhenResolved | Hypervel\Validation\Contracts\ValidatesWhenResolved | -| Illuminate\Contracts\Validation\ValidationRule | Hypervel\Validation\Contracts\Rule | -| Illuminate\Contracts\Validation\Validator | Hypervel\Validation\Contracts\Validator | -| Illuminate\Contracts\View\Engine | Hyperf\ViewEngine\Contract\EngineInterface | -| Illuminate\Contracts\View\Factory | Hyperf\ViewEngine\Contract\FactoryInterface | -| Illuminate\Contracts\View\View | Hyperf\ViewEngine\Contract\ViewInterface | - -::: note -Some components may not be 100% identical to their original Laravel counterparts. Be sure to compare and address any differences before replacing these dependencies. -::: - -### Replace PHPDocs with Native Types Declaration - -Most Laravel classes are **NOT** type-friendly. Laravel primarily uses PHPDoc annotations to specify the types of class properties and methods, and only the more recent packages use native type declarations. - -Unfortunately, the types specified via PHPDoc in Laravel are frequently inaccurate. If you simply convert PHPDoc types into native PHP type declarations, you'll encounter inconsistencies and omissions during execution. - -For example, the `item` parameter of the `getLine` function in `Translator::class` is documented as a **string** in the PHPDoc, but it should actually be `null|string`. This kind of incorrect typing is widespread in Laravel. - -```php -/** - * Retrieve a language line out the loaded array. - * - * @param string $namespace - * @param string $group - * @param string $locale - * @param string $item - * @param array $replace - * @return string|array|null - */ -protected function getLine($namespace, $group, $locale, $item, array $replace) -{ - $this->load($namespace, $group, $locale); - - $line = Arr::get($this->loaded[$namespace][$group][$locale], $item); - - // ... -} -``` - -Therefore, after implementation, you **must** rely on testing to ensure that your type declarations are correct. - -Here’s an example using `FileLoader` from the translation component: - -```php -files = $files; - - $this->paths = is_string($path) ? [$path] : $path; - } - - /** - * Load the messages for the given locale. - */ - public function load(string $locale, string $group, ?string $namespace = null): array - { - if ($group === '*' && $namespace === '*') { - return $this->loadJsonPaths($locale); - } - - if (is_null($namespace) || $namespace === '*') { - return $this->loadPaths($this->paths, $locale, $group); - } - - return $this->loadNamespaced($locale, $group, $namespace); - } - - // ... -} -``` - -Note the use of `declare(strict_types=1);` at the beginning of the file. This enforces strict type checking, preventing bugs through early error detection, improving reliability by eliminating implicit type conversions, and enhancing maintainability through explicit code intentions. - -In PHP 8.0+, constructor property promotion allows developers to define and initialize class properties directly within the constructor parameters, significantly reducing boilerplate code and improving both readability and maintainability. - -All properties and functions in the class are thus declared with native types and default values. - -### Isolating States for Coroutines - -As mentioned previously, Laravel components are not designed for coroutine environments. If you simply copy and paste code from Laravel to Hypervel, you are likely to encounter state leakage at some point. - -For instance, in the `Translator` class: - -```php -class Translator extends NamespacedItemResolver implements TranslatorContract -{ - // ... - - /** - * The default locale being used by the translator. - * - * @var string - */ - protected $locale; - - /** - * Get the default locale being used. - * - * @return string - */ - public function getLocale() - { - return $this->locale; - } - - /** - * Set the default locale. - * - * @param string $locale - * @return void - * - * @throws \InvalidArgumentException - */ - public function setLocale($locale) - { - if (Str::contains($locale, ['/', '\\'])) { - throw new InvalidArgumentException('Invalid characters present in locale.'); - } - - $this->locale = $locale; - } - - // ... -} -``` - -Such getter-setter designs are common for the `locale` property in stateless PHP environments. However, the `Translator` is intended to be a singleton, meaning its properties, including `locale`, will be shared across all requests. - -To mitigate this, Octane provides a `FlushLocaleState` listener to restore the states for `Translator`: - -```php -sandbox->make('config'); - - tap($event->sandbox->make('translator'), function ($translator) use ($config) { - $translator->setLocale($config->get('app.locale')); - $translator->setFallback($config->get('app.fallback_locale')); - }); - - $provider = tap(new CarbonServiceProvider($event->app))->updateLocale(); - - collect($event->sandbox->getProviders($provider)) - ->values() - ->whenNotEmpty(fn ($providers) => $providers->first()->setAppGetter(fn () => $event->sandbox)); - } -} -``` - -Unfortunately, this does not solve the issue in Hypervel, because each worker may serve multiple requests **simultaneously**. - -To resolve this, you need a **Coroutine-Level Container** for isolating state between coroutines, referred to in Hypervel as [**Context**](/docs/context). - -The code related to the `locale` property should be refactored as follows: - -```php -use Hypervel\Context\Context; - -class Translator extends NamespacedItemResolver implements TranslatorContract -{ - // ... - - /** - * Get the default locale being used. - */ - public function getLocale(): string - { - return (string) (Context::get('__translator.locale') ?? $this->locale); - } - - /** - * Set the default locale. - * - * @throws InvalidArgumentException - */ - public function setLocale(string $locale): void - { - if (Str::contains($locale, ['/', '\\'])) { - throw new InvalidArgumentException('Invalid characters present in locale.'); - } - - Context::set('__translator.locale', $locale); - } - - // ... -} -``` - -By storing the `locale` state in the coroutine context, each coroutine maintains its own state. - -### Migrate from Service Provider to Config Provider - -In Laravel, the Service Provider is a crucial component for binding and bootstrapping your services. Hypervel supports service providers in a similar way, but encourages developers to **rewrite** them as Config Providers to maintain compatibility with the Hyperf ecosystem. - -Consider the following `TranslationServiceProvider` from Laravel: - -```php -registerLoader(); - - $this->app->singleton('translator', function ($app) { - $loader = $app['translation.loader']; - - // When registering the translator component, we'll need to set the default - // locale as well as the fallback locale. So, we'll grab the application - // configuration so we can easily get both of these values from there. - $locale = $app->getLocale(); - - $trans = new Translator($loader, $locale); - - $trans->setFallback($app->getFallbackLocale()); - - return $trans; - }); - } - - /** - * Register the translation line loader. - * - * @return void - */ - protected function registerLoader() - { - $this->app->singleton('translation.loader', function ($app) { - return new FileLoader($app['files'], [__DIR__.'/lang', $app['path.lang']]); - }); - } - - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return ['translator', 'translation.loader']; - } -} -``` -In `TranslationServiceProvider`, two bindings — `translation.loader` and `translator` — are registered via `singleton`. In Hypervel, all dependencies in the container are singleton by default, so there is no need for an explicit `singleton` method. - -To implement these bindings in Hypervel's Config Provider, define them in the `dependencies` array within the `__invoke` function: - -```php - [ - LoaderContract::class => LoaderFactory::class, - TranslatorContract::class => TranslatorFactory::class, - ], - ]; - } -} -``` - -Of course, you can use closures as resolving callbacks, but defining dedicated factory classes makes your code more organized: - -```php -langPath() - : BASE_PATH . DIRECTORY_SEPARATOR . 'lang'; - - return new FileLoader( - $container->get(Filesystem::class), - [ - dirname(__DIR__) . DIRECTORY_SEPARATOR . 'lang', - $langPath, - ] - ); - } -} -``` - -```php -get(ConfigInterface::class); - - // When registering the translator component, we'll need to set the default - // locale as well as the fallback locale. So, we'll grab the application - // configuration so we can easily get both of these values from there. - $trans = new Translator( - $container->get(LoaderContract::class), - $config->get('app.locale', 'en') - ); - - $trans->setFallback($config->get('app.fallback_locale', 'en')); - - return $trans; - } -} -``` - -Hypervel does not require a `DeferrableProvider`, as this trait only provides a performance tweak for PHP’s traditional request lifecycle. In Hypervel, service/config providers are loaded at framework bootstrap and remain in memory for the application's lifetime. - -Don't forget to add your config provider for auto-discovery in `composer.json`: - -```json -"extra": { - "hyperf": { - "config": "Hypervel\\Translation\\ConfigProvider" - } -} -``` - -### Adjust Tests for The Package - -The final step is to port the unit tests from Laravel. Thanks to Laravel's comprehensive and reliable test suites, you can verify the correctness of your port by running these tests. - -You will likely discover more incorrect type declarations during this process. Fix all such issues as they arise. - -You may extend `Hypervel\Tests\TestCase::class` to leverage features such as assertion counting and automatic mockery closing. - -If you need a Hypervel environment for feature testing, you can see [Packages Toolkit](/docs/testbench) for more details. - -#### Tests in Coroutines - -Sometimes you may want to explicitly test your classes in a coroutine environment. In that case, you must execute tests within a coroutine container. - -For instance, since you’ve refactored the `setLocale` function using `Context`, you need to test that each coroutine maintains its own locale. Run the test in a coroutine context to observe the state isolation: - -> See [Running Tests in Coroutines](/docs/testing#running-tests-in-coroutines) for more details. - -```php -use Hepervel\Coroutine\run; - -public function testSetLocale() -{ - $translator = new Translator($this->getLoader(), 'en'); - - run(function () use ($translator) { - Coroutine::create(function () use ($translator) { - $translator->setLocale('fr'); - $this->assertSame('fr', $translator->getLocale()); - }); - }); - - $this->assertSame('en', $translator->getLocale()); -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/packages.md b/_archive/docs/0.3/docs/packages.md deleted file mode 100644 index e74af6ab6d..0000000000 --- a/_archive/docs/0.3/docs/packages.md +++ /dev/null @@ -1,425 +0,0 @@ -# Package Development -[[toc]] - -## Introduction - -Packages are the primary way of adding functionality to Hypervel. There are different types of packages. Some packages are stand-alone, meaning they work with any PHP framework. Carbon and PHPUnit are examples of stand-alone packages. Any of these packages may be used with Hypervel by requiring them in your `composer.json` file. - -On the other hand, other packages are specifically intended for use with Hypervel. These packages may have routes, controllers, views, and configuration specifically intended to enhance a Hypervel application. This guide primarily covers the development of those packages that are Hypervel specific. - -## Package Discovery - -In a Hypervel application's `config/app.php` configuration file, the `providers` option defines a list of service providers that should be loaded by Hypervel. When someone installs your package, you will typically want your service provider to be included in this list. Instead of requiring users to manually add your service provider to the list, you may define the provider in the `extra` section of your package's `composer.json` file. In addition to service providers, you may also list any [facades](/docs/facades) you would like to be registered: - -```json -"extra": { - "hypervel": { - "providers": [ - "Barryvdh\\Debugbar\\ServiceProvider" - ], - "aliases": { - "Debugbar": "Barryvdh\\Debugbar\\Facade" - } - } -}, -``` - -Once your package has been configured for discovery, Hypervel will automatically register its service providers and facades when it is installed, creating a convenient installation experience for your package's users. - -#### Opting Out of Package Discovery - -If you are the consumer of a package and would like to disable package discovery for a package, you may list the package name in the `extra` section of your application's `composer.json` file: - -```json -"extra": { - "hypervel": { - "dont-discover": [ - "barryvdh/laravel-debugbar" - ] - } -}, -``` - -You may disable package discovery for all packages using the `*` character inside of your application's `dont-discover` directive: - -```json -"extra": { - "hypervel": { - "dont-discover": [ - "*" - ] - } -}, -``` - -::: important -Hypervel also supports `Config Provider` for the compatibility with Hyperf. We encourage using config providers that we can empower Hyperf's ecosystem at same time. -::: - -## Config Providers - -Config providers are placed in each root directory of the component. These providers will supply all the configuration information of the corresponding component, which will be started by the Hypervel when loaded. - -The final configuration information in config providers will be merged into the corresponding implementation class of `Hyperf\Contract\ConfigInterface`. This process enables the configuration initialization of each component when used under the Hypervel. - -A sample config provider looks like below: - -```php - [], - 'annotations' => [ - 'scan' => [ - 'paths' => [ - __DIR__, - ], - ], - ], - 'listeners' => [], - 'publish' => [ - [ - 'id' => 'config', - 'description' => 'description of this config file.', - 'source' => __DIR__ . '/../publish/file.php', - 'destination' => BASE_PATH . '/config/autoload/file.php', - ], - ], - ]; - } -} -``` - -* `dependencies`: -This key is used to define dependency injection configurations. It will be merged into the `config/dependencies.php` file. You can define your dependency bindings here, which is equivalent to binding interfaces in service containers. - -* `annotations`: -This key is used to configure annotation scanning. It will be merged into the `config/annotations.php` file. In this example, it sets the scan path to the current directory. - -* `commands`: -This key is used to define command classes. It will be merged into commands config array, which can also be understood as corresponding to the `config/commands.php` file. And these commands will be registered when the framework bootstrapped. - -* `listeners`: -This key functions similarly to commands and is used to define listeners. - -* `publish`: -This key is used to define the component's default configuration files. When the publish command is executed, the file corresponding to the source will be copied to the path corresponding to the destination. In this example, it defines a configuration file with the ID 'config', including a description, source file path, and destination file path. - -* `Other configurations`: -In addition to the predefined configuration keys above, you can define other configurations. These configurations will ultimately be merged into the configuration corresponding to `ConfigInterface`. - -::: note -Unlike service providers, config providers don't have direct approach to register your resources to the application. You can only declare your resource assets to be published. - -Also, resource path in Hyperf framework has strong coupling with `BASE_PATH` constant. If you use helper functions like `base_path`, `storage_path`, `resource_path`, etc, it will only work in Hypervel. -::: - -## Service Providers - -[Service providers](/docs/providers) are the connection point between your package and Hypervel. A service provider is responsible for binding things into Hypervel's [service container](/docs/container) and informing Hypervel where to load package resources such as views, configuration, and language files. - -A service provider extends the `Hypervel\Support\ServiceProvider` class and contains two methods: `register` and `boot`. The base `ServiceProvider` class is located in the `hypervel/support` Composer package, which you should add to your own package's dependencies. To learn more about the structure and purpose of service providers, check out [their documentation](/docs/providers). - -## Resources - -### Configuration - -Typically, you will need to publish your package's configuration file to the application's `config` directory. This will allow users of your package to easily override your default configuration options. To allow your configuration files to be published, call the `publishes` method from the `boot` method of your service provider: - -```php -/** - * Bootstrap any package services. - */ -public function boot(): void -{ - $this->publishes([ - __DIR__.'/../config/courier.php' => config_path('courier.php'), - ]); -} -``` - -Now, when users of your package execute Hypervel's `vendor:publish` command, your file will be copied to the specified publish location. Once your configuration has been published, its values may be accessed like any other configuration file: - -```php -$value = config('courier.option'); -``` - -#### Default Package Configuration - -You may also merge your own package configuration file with the application's published copy. This will allow your users to define only the options they actually want to override in the published copy of the configuration file. To merge the configuration file values, use the `mergeConfigFrom` method within your service provider's `register` method. - -The `mergeConfigFrom` method accepts the path to your package's configuration file as its first argument and the name of the application's copy of the configuration file as its second argument: - -```php -/** - * Register any application services. - */ -public function register(): void -{ - $this->mergeConfigFrom( - __DIR__.'/../config/courier.php', 'courier' - ); -} -``` - -::: warning -This method only merges the first level of the configuration array. If your users partially define a multi-dimensional configuration array, the missing options will not be merged. -::: - -### Routes - -If your package contains routes, you may load them using the `loadRoutesFrom` method. This method will automatically determine if the application's routes are cached and will not load your routes file if the routes have already been cached: - -```php -/** - * Bootstrap any package services. - */ -public function boot(): void -{ - $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); -} -``` - -### Migrations - -If your package contains [database migrations](/docs/migrations), you may use the `loadMigrationsFrom` method to inform Laravel how to load them. The `loadMigrationsFrom` method accepts the path to your package's migrations as its only argument: - -```php -/** - * Bootstrap any package services. - */ -public function boot(): void -{ - $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); -} -``` - -Once your package's migrations have been registered, they will automatically be run when the `php artisan migrate` command is executed. You do not need to export them to the application's `database/migrations` directory. - -### Language Files - -If your package contains [language files](/docs/localization), you may use the `loadTranslationsFrom` method to inform Hypervel how to load them. For example, if your package is named `courier`, you should add the following to your service provider's `boot` method: - -```php -/** - * Bootstrap any package services. - */ -public function boot(): void -{ - $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier'); -} -``` - -Package translation lines are referenced using the `package::file.line` syntax convention. So, you may load the `courier` package's `welcome` line from the `messages` file like so: - -```php -echo trans('courier::messages.welcome'); -``` - -You can register JSON translation files for your package using the `loadJsonTranslationsFrom` method. This method accepts the path to the directory that contains your package's JSON translation files: - -```php -/** - * Bootstrap any package services. - */ -public function boot(): void -{ - $this->loadJsonTranslationsFrom(__DIR__.'/../lang'); -} -``` - -#### Publishing Language Files - -If you would like to publish your package's language files to the application's `lang/vendor` directory, you may use the service provider's `publishes` method. The `publishes` method accepts an array of package paths and their desired publish locations. For example, to publish the language files for the `courier` package, you may do the following: - -```php -/** - * Bootstrap any package services. - */ -public function boot(): void -{ - $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier'); - - $this->publishes([ - __DIR__.'/../lang' => $this->app->langPath('vendor/courier'), - ]); -} -``` - -Now, when users of your package execute Hypervel's `vendor:publish` Artisan command, your package's language files will be published to the specified publish location. - -### Views - -To register your package's [views](/docs/views) with Laravel, you need to tell Hypervel where the views are located. You may do this using the service provider's `loadViewsFrom` method. The `loadViewsFrom` method accepts two arguments: the path to your view templates and your package's name. For example, if your package's name is `courier`, you would add the following to your service provider's `boot` method: - -```php -/** - * Bootstrap any package services. - */ -public function boot(): void -{ - $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier'); -} -``` - -Package views are referenced using the `package::view` syntax convention. So, once your view path is registered in a service provider, you may load the `dashboard` view from the `courier` package like so: - -```php -Route::get('/dashboard', function () { - return view('courier::dashboard'); -}); -``` - -#### Overriding Package Views - -When you use the `loadViewsFrom` method, Hypervel actually registers two locations for your views: the application's `resources/views/vendor` directory and the directory you specify. So, using the `courier` package as an example, Hypervel will first check if a custom version of the view has been placed in the `resources/views/vendor/courier` directory by the developer. Then, if the view has not been customized, Hypervel will search the package view directory you specified in your call to `loadViewsFrom`. This makes it easy for package users to customize / override your package's views. - -#### Publishing Views - -If you would like to make your views available for publishing to the application's `resources/views/vendor` directory, you may use the service provider's `publishes` method. The `publishes` method accepts an array of package view paths and their desired publish locations: - -```php -/** - * Bootstrap the package services. - */ -public function boot(): void -{ - $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier'); - - $this->publishes([ - __DIR__.'/../resources/views' => resource_path('views/vendor/courier'), - ]); -} -``` - -Now, when users of your package execute Hypervel's `vendor:publish` Artisan command, your package's views will be copied to the specified publish location. - -### View Components - -If you are building a package that utilizes Blade components or placing components in non-conventional directories, you will need to manually register your component class and its HTML tag alias so that Hypervel knows where to find the component. You should typically register your components in the `boot` method of your package's service provider: - -```php -use Hypervel\Support\Facades\Blade; -use VendorPackage\View\Components\AlertComponent; - -/** - * Bootstrap your package's services. - */ -public function boot(): void -{ - Blade::component('package-alert', AlertComponent::class); -} -``` - -Once your component has been registered, it may be rendered using its tag alias: - -```html - -``` - -#### Autoloading Package Components - -Alternatively, you may use the `componentNamespace` method to autoload component classes by convention. For example, a `Nightshade` package might have `Calendar` and `ColorPicker` components that reside within the `Nightshade\Views\Components` namespace: - -```php -use Hypervel\Support\Facades\Blade; - -/** - * Bootstrap your package's services. - */ -public function boot(): void -{ - Blade::componentNamespace('Nightshade\\Views\\Components', 'nightshade'); -} -``` - -This will allow the usage of package components by their vendor namespace using the `package-name::` syntax: - -```html - - -``` - -Blade will automatically detect the class that's linked to this component by pascal-casing the component name. Subdirectories are also supported using "dot" notation. - -#### Anonymous Components - -If your package contains anonymous components, they must be placed within a `components` directory of your package's "views" directory (as specified by the [`loadViewsFrom` method](#views)). Then, you may render them by prefixing the component name with the package's view namespace: - -```html - -``` - -## Commands - -To register your package's Artisan commands with Hypervel, you may use the `commands` method. This method expects an array of command class names. Once the commands have been registered, you may execute them using the [Artisan CLI](/docs/artisan): - -```php -use Courier\Console\Commands\InstallCommand; -use Courier\Console\Commands\NetworkCommand; - -/** - * Bootstrap any package services. - */ -public function boot(): void -{ - $this->commands([ - InstallCommand::class, - NetworkCommand::class, - ]); -} -``` - -## Public Assets - -Your package may have assets such as JavaScript, CSS, and images. To publish these assets to the application's `public` directory, use the service provider's `publishes` method. In this example, we will also add a `public` asset group tag, which may be used to easily publish groups of related assets: - -```php -/** - * Bootstrap any package services. - */ -public function boot(): void -{ - $this->publishes([ - __DIR__.'/../public' => public_path('vendor/courier'), - ], 'public'); -} -``` - -Now, when your package's users execute the `vendor:publish` command, your assets will be copied to the specified publish location. Since users will typically need to overwrite the assets every time the package is updated, you may use the `--force` flag: - -```shell:no-line-numbers -php artisan vendor:publish courier-package --tag=public --force -``` - -## Publishing File Groups - -You may want to publish groups of package assets and resources separately. For instance, you might want to allow your users to publish your package's configuration files without being forced to publish your package's assets. You may do this by "tagging" them when calling the `publishes` method from a package's service provider. For example, let's use tags to define two publish groups for the `courier` package (`courier-config` and `courier-migrations`) in the `boot` method of the package's service provider: - -```php -/** - * Bootstrap any package services. - */ -public function boot(): void -{ - $this->publishes([ - __DIR__.'/../config/package.php' => config_path('package.php') - ], 'courier-config'); - - $this->publishes([ - __DIR__.'/../database/migrations/' => database_path('migrations') - ], 'courier-migrations'); -} -``` - -Now your users may publish these groups separately by referencing their tag when executing the `vendor:publish` command: - -```shell:no-line-numbers -php artisan vendor:publish courier-package --tag=courier-config -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/pagination.md b/_archive/docs/0.3/docs/pagination.md deleted file mode 100644 index aad8d7621d..0000000000 --- a/_archive/docs/0.3/docs/pagination.md +++ /dev/null @@ -1,312 +0,0 @@ -# Database: Pagination -[[toc]] - -## Introduction - -In other frameworks, pagination can be very painful. We hope Hypervel's approach to pagination will be a breath of fresh air. Hypervel's paginator is integrated with the [query builder](/docs/queries) and [Eloquent ORM](/docs/eloquent) and provides convenient, easy-to-use pagination of database records with zero configuration. - -## Basic Usage - -### Paginating Query Builder Results - -There are several ways to paginate items. The simplest is by using the `paginate` method on the [query builder](/docs/queries) or an [Eloquent query](/docs/eloquent). The `paginate` method automatically takes care of setting the query's "limit" and "offset" based on the current page being viewed by the user. By default, the current page is detected by the value of the `page` query string argument on the HTTP request. This value is automatically detected by Hypervel Hyperf, and is also automatically inserted into links generated by the paginator. - -In this example, the only argument passed to the `paginate` method is the number of items you would like displayed "per page". In this case, let's specify that we would like to display `15` items per page: - -```php - DB::table('users')->paginate(15) - ]); - } -} -``` - -#### Simple Pagination - -The `paginate` method counts the total number of records matched by the query before retrieving the records from the database. This is done so that the paginator knows how many pages of records there are in total. However, if you do not plan to show the total number of pages in your application's UI then the record count query is unnecessary. - -Therefore, if you only need to display simple "Next" and "Previous" links in your application's UI, you may use the `simplePaginate` method to perform a single, efficient query: - -```php -$users = DB::table('users')->simplePaginate(15); -``` - -### Paginating Eloquent Results - -You may also paginate [Eloquent](/docs/eloquent) queries. In this example, we will paginate the `App\Models\User` model and indicate that we plan to display 15 records per page. As you can see, the syntax is nearly identical to paginating query builder results: - -```php -use App\Models\User; - -$users = User::paginate(15); -``` - -Of course, you may call the `paginate` method after setting other constraints on the query, such as `where` clauses: - -```php -$users = User::where('votes', '>', 100)->paginate(15); -``` - -You may also use the `simplePaginate` method when paginating Eloquent models: - -```php -$users = User::where('votes', '>', 100)->simplePaginate(15); -``` - -Similarly, you may use the `cursorPaginate` method to cursor paginate Eloquent models: - -```php -$users = User::where('votes', '>', 100)->cursorPaginate(15); -``` - -#### Multiple Paginator Instances per Page - -Sometimes you may need to render two separate paginators on a single screen that is rendered by your application. However, if both paginator instances use the `page` query string parameter to store the current page, the two paginator's will conflict. To resolve this conflict, you may pass the name of the query string parameter you wish to use to store the paginator's current page via the third argument provided to the `paginate`, `simplePaginate`, and `cursorPaginate` methods: - -```php -use App\Models\User; - -$users = User::where('votes', '>', 100)->paginate( - $perPage = 15, $columns = ['*'], $pageName = 'users' -); -``` - -### Cursor Pagination - -While `paginate` and `simplePaginate` create queries using the SQL "offset" clause, cursor pagination works by constructing "where" clauses that compare the values of the ordered columns contained in the query, providing the most efficient database performance available amongst all of Hypervel's pagination methods. This method of pagination is particularly well-suited for large data-sets and "infinite" scrolling user interfaces. - -Unlike offset based pagination, which includes a page number in the query string of the URLs generated by the paginator, cursor based pagination places a "cursor" string in the query string. The cursor is an encoded string containing the location that the next paginated query should start paginating and the direction that it should paginate: - -```text -http://localhost/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0 -``` - -You may create a cursor based paginator instance via the `cursorPaginate` method offered by the query builder. This method returns an instance of `Hyperf\Pagination\CursorPaginator`: - -```php -$users = DB::table('users')->orderBy('id')->cursorPaginate(15); -``` - -Once you have retrieved a cursor paginator instance, you may [display the pagination results](#displaying-pagination-results) as you typically would when using the `paginate` and `simplePaginate` methods. For more information on the instance methods offered by the cursor paginator, please consult the [cursor paginator instance method documentation](#cursor-paginator-instance-methods). - -::: warning -Your query must contain an "order by" clause in order to take advantage of cursor pagination. In addition, the columns that the query are ordered by must belong to the table you are paginating. -::: - -#### Cursor vs. Offset Pagination - -To illustrate the differences between offset pagination and cursor pagination, let's examine some example SQL queries. Both of the following queries will both display the "second page" of results for a `users` table ordered by `id`: - -```sql -# Offset Pagination... -select * from users order by id asc limit 15 offset 15; - -# Cursor Pagination... -select * from users where id > 15 order by id asc limit 15; -``` - -The cursor pagination query offers the following advantages over offset pagination: - -- For large data-sets, cursor pagination will offer better performance if the "order by" columns are indexed. This is because the "offset" clause scans through all previously matched data. -- For data-sets with frequent writes, offset pagination may skip records or show duplicates if results have been recently added to or deleted from the page a user is currently viewing. - -However, cursor pagination has the following limitations: - -- Like `simplePaginate`, cursor pagination can only be used to display "Next" and "Previous" links and does not support generating links with page numbers. -- It requires that the ordering is based on at least one unique column or a combination of columns that are unique. Columns with `null` values are not supported. -- Query expressions in "order by" clauses are supported only if they are aliased and added to the "select" clause as well. -- Query expressions with parameters are not supported. - -### Manually Creating a Paginator - -Sometimes you may wish to create a pagination instance manually, passing it an array of items that you already have in memory. You may do so by creating either an `Hyperf\Pagination\Paginator`, `Hyperf\Pagination\LengthAwarePaginator` or `Hyperf\Pagination\CursorPaginator` instance, depending on your needs. - -The `Paginator` and `CursorPaginator` classes do not need to know the total number of items in the result set; however, because of this, these classes do not have methods for retrieving the index of the last page. The `LengthAwarePaginator` accepts almost the same arguments as the `Paginator`; however, it requires a count of the total number of items in the result set. - -In other words, the `Paginator` corresponds to the `simplePaginate` method on the query builder, the `CursorPaginator` corresponds to the `cursorPaginate` method, and the `LengthAwarePaginator` corresponds to the `paginate` method. - -::: warning -When manually creating a paginator instance, you should manually "slice" the array of results you pass to the paginator. If you're unsure how to do this, check out the [array_slice](https://secure.php.net/manual/en/function.array-slice.php) PHP function. -::: - -### Customizing Pagination URLs - -By default, links generated by the paginator will match the current request's URI. However, the paginator's `withPath` method allows you to customize the URI used by the paginator when generating links. For example, if you want the paginator to generate links like `http://example.com/admin/users?page=N`, you should pass `/admin/users` to the `withPath` method: - -```php -use App\Models\User; - -Route::get('/users', function () { - $users = User::paginate(15); - - $users->withPath('/admin/users'); - - // ... -}); -``` - -#### Appending Query String Values - -You may append to the query string of pagination links using the `appends` method. For example, to append `sort=votes` to each pagination link, you should make the following call to `appends`: - -```php -use App\Models\User; - -Route::get('/users', function () { - $users = User::paginate(15); - - $users->appends(['sort' => 'votes']); - - // ... -}); -``` - -You may use the `withQueryString` method if you would like to append all of the current request's query string values to the pagination links: - -```php -$users = User::paginate(15)->withQueryString(); -``` - -#### Appending Hash Fragments - -If you need to append a "hash fragment" to URLs generated by the paginator, you may use the `fragment` method. For example, to append `#users` to the end of each pagination link, you should invoke the `fragment` method like so: - -```php -$users = User::paginate(15)->fragment('users'); -``` - -## Displaying Pagination Results - -When calling the `paginate` method, you will receive an instance of `Hyperf\Pagination\LengthAwarePaginator`, while calling the `simplePaginate` method returns an instance of `Hyperf\Pagination\Paginator`. And, finally, calling the `cursorPaginate` method returns an instance of `Hyperf\Pagination\CursorPaginator`. - -These objects provide several methods that describe the result set. In addition to these helper methods, the paginator instances are iterators and may be looped as an array. So, once you have retrieved the results, you may display the results and render the page links using [Blade](/docs/blade): - -```html -
      - @foreach ($users as $user) - {{ $user->name }} - @endforeach -
      - -{{ $users->links() }} -``` - -The `links` method will render the links to the rest of the pages in the result set. Each of these links will already contain the proper `page` query string variable. - -### Adjusting the Pagination Link Window - -When the paginator displays pagination links, the current page number is displayed as well as links for the three pages before and after the current page. Using the `onEachSide` method, you may control how many additional links are displayed on each side of the current page within the middle, sliding window of links generated by the paginator: - -```html -{{ $users->onEachSide(5)->links() }} -``` - -### Converting Results to JSON - -The Hypervel paginator classes implement the `Hyperf\Contract\Jsonable` Interface contract and expose the `toJson` method, so it's very easy to convert your pagination results to JSON. You may also convert a paginator instance to JSON by returning it from a route or controller action: - -```php -use App\Models\User; - -Route::get('/users', function () { - return User::paginate(); -}); -``` - -The JSON from the paginator will include meta information such as `total`, `current_page`, `last_page`, and more. The result records are available via the `data` key in the JSON array. Here is an example of the JSON created by returning a paginator instance from a route: - -```json -{ - "total": 50, - "per_page": 15, - "current_page": 1, - "last_page": 4, - "first_page_url": "http://laravel.app?page=1", - "last_page_url": "http://laravel.app?page=4", - "next_page_url": "http://laravel.app?page=2", - "prev_page_url": null, - "path": "http://laravel.app", - "from": 1, - "to": 15, - "data":[ - { - // Record... - }, - { - // Record... - } - ] -} -``` - -## Paginator / LengthAwarePaginator Instance Methods - -Each paginator instance provides additional pagination information via the following methods: - -
      - -| Method | Description | -| --- | --- | -| `$paginator->count()` | Get the number of items for the current page. | -| `$paginator->currentPage()` | Get the current page number. | -| `$paginator->firstItem()` | Get the result number of the first item in the results. | -| `$paginator->getOptions()` | Get the paginator options. | -| `$paginator->getUrlRange($start, $end)` | Create a range of pagination URLs. | -| `$paginator->hasPages()` | Determine if there are enough items to split into multiple pages. | -| `$paginator->hasMorePages()` | Determine if there are more items in the data store. | -| `$paginator->items()` | Get the items for the current page. | -| `$paginator->lastItem()` | Get the result number of the last item in the results. | -| `$paginator->lastPage()` | Get the page number of the last available page. (Not available when using `simplePaginate`). | -| `$paginator->nextPageUrl()` | Get the URL for the next page. | -| `$paginator->onFirstPage()` | Determine if the paginator is on the first page. | -| `$paginator->perPage()` | The number of items to be shown per page. | -| `$paginator->previousPageUrl()` | Get the URL for the previous page. | -| `$paginator->total()` | Determine the total number of matching items in the data store. (Not available when using `simplePaginate`). | -| `$paginator->url($page)` | Get the URL for a given page number. | -| `$paginator->getPageName()` | Get the query string variable used to store the page. | -| `$paginator->setPageName($name)` | Set the query string variable used to store the page. | -| `$paginator->through($callback)` | Transform each item using a callback. | - -
      - -## Cursor Paginator Instance Methods - -Each cursor paginator instance provides additional pagination information via the following methods: - -
      - -| Method | Description | -| ------------------------------- | ----------------------------------------------------------------- | -| `$paginator->count()` | Get the number of items for the current page. | -| `$paginator->cursor()` | Get the current cursor instance. | -| `$paginator->getOptions()` | Get the paginator options. | -| `$paginator->hasPages()` | Determine if there are enough items to split into multiple pages. | -| `$paginator->hasMorePages()` | Determine if there are more items in the data store. | -| `$paginator->getCursorName()` | Get the query string variable used to store the cursor. | -| `$paginator->items()` | Get the items for the current page. | -| `$paginator->nextCursor()` | Get the cursor instance for the next set of items. | -| `$paginator->nextPageUrl()` | Get the URL for the next page. | -| `$paginator->onFirstPage()` | Determine if the paginator is on the first page. | -| `$paginator->onLastPage()` | Determine if the paginator is on the last page. | -| `$paginator->perPage()` | The number of items to be shown per page. | -| `$paginator->previousCursor()` | Get the cursor instance for the previous set of items. | -| `$paginator->previousPageUrl()` | Get the URL for the previous page. | -| `$paginator->setCursorName()` | Set the query string variable used to store the cursor. | -| `$paginator->url($cursor)` | Get the URL for a given cursor instance. | - -
      \ No newline at end of file diff --git a/_archive/docs/0.3/docs/permission.md b/_archive/docs/0.3/docs/permission.md deleted file mode 100644 index e9af2fdfca..0000000000 --- a/_archive/docs/0.3/docs/permission.md +++ /dev/null @@ -1,676 +0,0 @@ -# Hypervel Permission - -[[toc]] - -## Introduction - -Modern web applications often need to control what users can and cannot do. Whether you're building a content management system where editors can publish articles but only admins can manage users, or an e-commerce platform where different staff members have access to different parts of the system, you'll need a robust way to manage permissions and user roles. - -The Hypervel Permission package provides a flexible and powerful solution for implementing role-based access control (RBAC) in your applications. This package is ported from [Spatie's Permission package](https://spatie.be/docs/laravel-permission) but optimized for the Hypervel framework with advanced caching capabilities and support for modern PHP features. Instead of hardcoding permission checks throughout your codebase, you can define roles like "editor" or "admin", assign specific permissions to those roles, and then simply check if a user has the required role or permission. The package handles all the complex database queries and caching behind the scenes. - -What makes this package particularly powerful is its flexibility - you can assign roles to users for broad access control, assign specific permissions for fine-grained control, or even use "forbidden permissions" to explicitly deny access to certain actions. All permission checks are optimized with intelligent caching, so you don't have to worry about performance even with complex permission hierarchies. - -## Installation - -You can install the permission package via Composer: - -```bash -composer require hypervel/permission -``` - -The package will automatically register its service provider. - -### Publishing Assets - -Publish the migration and configuration files: - -```bash -php artisan vendor:publish "Hypervel\Permission\PermissionServiceProvider" -``` - -Or publish them separately: - -```bash -# Publish config only -php artisan vendor:publish "Hypervel\Permission\PermissionServiceProvider" --tag="permission-config" - -# Publish migrations only -php artisan vendor:publish "Hypervel\Permission\PermissionServiceProvider" --tag="permission-migrations" -``` - -### Running Migrations - -After publishing, run the migrations to create the necessary tables: - -```bash -php artisan migrate -``` - -This will create the following tables: -- `roles` - stores role information -- `permissions` - stores permission information -- `role_has_permissions` - pivot table for role-permission relationships -- `owner_has_permissions` - polymorphic table for model-permission relationships -- `owner_has_roles` - polymorphic table for model-role relationships - -## Configuration - -The package configuration file `config/permission.php` allows you to customize various aspects: - -### Models - -You can specify custom models for roles and permissions: - -```php -'models' => [ - 'role' => \App\Models\Role::class, - 'permission' => \App\Models\Permission::class, -], -``` - -### Table Names - -Customize table names to match your database schema: - -```php -'table_names' => [ - 'roles' => 'roles', - 'permissions' => 'permissions', - 'role_has_permissions' => 'role_has_permissions', - 'owner_has_permissions' => 'owner_has_permissions', - 'owner_has_roles' => 'owner_has_roles', -], -``` - -### Column Names - -Customize column names used in relationships: - -```php -'column_names' => [ - 'role_pivot_key' => 'role_id', - 'permission_pivot_key' => 'permission_id', - 'owner_morph_key' => 'owner_id', - 'owner_name' => 'owner', -], -``` - -### Cache Configuration - -Configure caching behavior for optimal performance: - -```php -'cache' => [ - 'expiration_seconds' => 86400, // 24 hours - 'keys' => [ - 'roles' => 'hypervel.permission.cache.roles', - 'owner_roles' => 'hypervel.permission.cache.owner.roles', - 'owner_permissions' => 'hypervel.permission.cache.owner.permissions', - ], - 'store' => env('PERMISSION_CACHE_STORE', 'default'), -], -``` - -## Basic Usage - -### Adding Traits to Models - -To use permissions and roles with your models, add the appropriate traits: - -#### For Users (or any model that should have roles and permissions): - -```php -namespace App\Models; - -use Hypervel\Database\Eloquent\Model; -use Hypervel\Permission\Traits\HasRole; - -class User extends Model -{ - use HasRole; // This also includes HasPermission trait - - // Your model code... -} -``` - -#### For Custom Permission Model: - -```php -namespace App\Models; - -use Hypervel\Permission\Models\Permission as BasePermission; - -class Permission extends BasePermission -{ - // Add custom methods or properties -} -``` - -#### For Custom Role Model: - -```php -namespace App\Models; - -use Hypervel\Permission\Models\Role as BaseRole; - -class Role extends BaseRole -{ - // Add custom methods or properties -} -``` - -### Creating Permissions and Roles - -#### Creating Permissions - -```php -use Hypervel\Permission\Models\Permission; - -// Create permissions -$permission = Permission::create(['name' => 'edit articles', 'guard_name' => 'web']); -$permission = Permission::create(['name' => 'delete articles', 'guard_name' => 'web']); -$permission = Permission::create(['name' => 'publish articles', 'guard_name' => 'web']); -``` - -#### Creating Roles - -```php -use Hypervel\Permission\Models\Role; - -// Create roles -$role = Role::create(['name' => 'writer', 'guard_name' => 'web']); -$role = Role::create(['name' => 'editor', 'guard_name' => 'web']); -$role = Role::create(['name' => 'admin', 'guard_name' => 'web']); -``` - -#### Assigning Permissions to Roles - -```php -// Assign permissions to roles -$role = Role::findByName('writer'); -$role->givePermissionTo('edit articles'); - -// Assign multiple permissions -$role->givePermissionTo(['edit articles', 'delete articles']); - -// Sync permissions (replaces all existing permissions) -$role->syncPermissions(['edit articles', 'publish articles']); -``` - -## Working with Roles - -### Assigning Roles to Models - -```php -// Assign a role to a user -$user->assignRole('writer'); - -// Assign multiple roles -$user->assignRole(['writer', 'editor']); - -// You can also use IDs -$user->assignRole(1); - -// Sync roles (replaces all existing roles) -$user->syncRoles(['writer', 'editor']); -``` - -### Checking Roles - -```php -// Check if user has a specific role -if ($user->hasRole('writer')) { - // User has writer role -} - -// Check if user has any of the specified roles -if ($user->hasAnyRoles(['writer', 'editor'])) { - // User has at least one of these roles -} - -// Check if user has all specified roles -if ($user->hasAllRoles(['writer', 'editor'])) { - // User has both roles -} - -// Get specific roles that match -$matchingRoles = $user->onlyRoles(['writer', 'admin']); -``` - -### Removing Roles - -```php -// Remove a role -$user->removeRole('writer'); - -// Remove multiple roles -$user->removeRole(['writer', 'editor']); -``` - -## Working with Permissions - -### Assigning Permissions to Models - -```php -// Give permission directly to user -$user->givePermissionTo('edit articles'); - -// Give multiple permissions -$user->givePermissionTo(['edit articles', 'delete articles']); - -// Sync permissions (replaces all existing permissions) -$user->syncPermissions(['edit articles', 'publish articles']); -``` - -### Checking Permissions - -```php -// Check if user has a specific permission -if ($user->hasPermission('edit articles')) { - // User has permission (either direct or via role) -} - -// Check direct permissions only -if ($user->hasDirectPermission('edit articles')) { - // User has direct permission (not via role) -} - -// Check if user has permission via roles -if ($user->hasPermissionViaRoles('edit articles')) { - // User has permission through assigned roles -} - -// Check if user has any of the specified permissions -if ($user->hasAnyPermissions(['edit articles', 'delete articles'])) { - // User has at least one permission -} - -// Check if user has all specified permissions -if ($user->hasAllPermissions(['edit articles', 'delete articles'])) { - // User has all permissions -} -``` - -### Forbidden Permissions - -The package supports forbidden permissions that explicitly deny access: - -```php -// Give forbidden permission (denies access) -$user->giveForbiddenTo('delete articles'); - -// Check if user has forbidden permission -if ($user->hasForbiddenPermission('delete articles')) { - // Access is explicitly denied -} - -// Forbidden permissions override allowed permissions -$user->givePermissionTo('delete articles'); -$user->giveForbiddenTo('delete articles'); -// hasPermission('delete articles') will return false -``` - -### Revoking Permissions - -```php -// Revoke permission -$user->revokePermissionTo('edit articles'); - -// Revoke multiple permissions -$user->revokePermissionTo(['edit articles', 'delete articles']); -``` - -### Getting All Permissions - -```php -// Get all permissions (direct + via roles, excluding forbidden) -$permissions = $user->getAllPermissions(); - -// Get permissions via roles only -$rolePermissions = $user->getPermissionsViaRoles(); -``` - -## Using Enums - -The package supports PHP 8.1+ enums for type-safe permission and role management: - -### Backed Enums - -```php -enum Permission: string -{ - case EDIT_ARTICLES = 'edit articles'; - case DELETE_ARTICLES = 'delete articles'; - case PUBLISH_ARTICLES = 'publish articles'; -} - -enum Role: string -{ - case WRITER = 'writer'; - case EDITOR = 'editor'; - case ADMIN = 'admin'; -} - -// Usage with enums -$user->assignRole(Role::WRITER); -$user->givePermissionTo(Permission::EDIT_ARTICLES); - -if ($user->hasPermission(Permission::EDIT_ARTICLES)) { - // User can edit articles -} -``` - -### Unit Enums - -```php -enum SimplePermission -{ - case EDIT_ARTICLES; - case DELETE_ARTICLES; - case PUBLISH_ARTICLES; -} - -// The enum name will be used as the permission name -$user->givePermissionTo(SimplePermission::EDIT_ARTICLES); -``` - -## Middleware - -The package provides middleware for protecting routes based on roles and permissions. - -### Permission Middleware - -```php -use Hypervel\Permission\Middlewares\PermissionMiddleware; - -// In your route definitions -$router->get('/admin', [AdminController::class, 'index'], [ - 'middleware' => [PermissionMiddleware::using('view admin')], -]); - -// Multiple permissions (user needs ANY of them) -$router->get('/posts/edit', [PostController::class, 'edit'], [ - 'middleware' => [PermissionMiddleware::using('edit articles', 'edit all')], -]); -``` - -### Role Middleware - -```php -use Hypervel\Permission\Middlewares\RoleMiddleware; - -// In your route definitions -$router->get('/admin', [AdminController::class, 'index'], [ - 'middleware' => [RoleMiddleware::using('admin')], -]); - -// Multiple roles (user needs ANY of them) -$router->get('/editor', [EditorController::class, 'index'], [ - 'middleware' => [RoleMiddleware::using('editor', 'admin')], -]); -``` - -### Using with Enums - -```php -$router->get('/admin', [AdminController::class, 'index'], [ - 'middleware' => [PermissionMiddleware::using(Permission::VIEW_ADMIN)], -]); - -$router->get('/editor', [EditorController::class, 'index'], [ - 'middleware' => [RoleMiddleware::using(Role::EDITOR, Role::ADMIN)], -]); -``` - -## Console Commands - -### Viewing Permissions and Roles - -Use the built-in command to view the permission matrix: - -```bash -# Show all permissions and roles -php artisan permission:show - -# Show for specific guard -php artisan permission:show web - -# Use different table style -php artisan permission:show web compact -``` - -## Advanced Usage - -### Custom Guards - -The package supports multiple guards: - -```php -// Create permissions for different guards -Permission::create(['name' => 'view admin', 'guard_name' => 'web']); -Permission::create(['name' => 'api access', 'guard_name' => 'api']); - -// Roles with different guards -Role::create(['name' => 'admin', 'guard_name' => 'web']); -Role::create(['name' => 'api-user', 'guard_name' => 'api']); -``` - -### Polymorphic Relationships - -The package uses polymorphic relationships, so any model can have roles and permissions: - -```php -class Team extends Model -{ - use HasRole; -} - -$team = Team::find(1); -$team->assignRole('project-manager'); -$team->givePermissionTo('manage projects'); -``` - -### Working with the Permission Manager - -The `PermissionManager` class provides advanced functionality: - -```php -use Hypervel\Permission\PermissionManager; - -$manager = app(PermissionManager::class); - -// Get role and permission classes -$roleClass = $manager->getRoleClass(); -$permissionClass = $manager->getPermissionClass(); - -// Cache management -$manager->clearAllRolesPermissionsCache(); -$manager->clearOwnerCache(User::class, $userId); - -// Get cached data -$allRolesWithPermissions = $manager->getAllRolesWithPermissions(); -$cachedRoles = $manager->getOwnerCachedRoles(User::class, $userId); -$cachedPermissions = $manager->getOwnerCachedPermissions(User::class, $userId); -``` - -### Custom Models - -If you want to extend the base models: - -```php -// Custom Permission Model -class CustomPermission extends \Hypervel\Permission\Models\Permission -{ - protected array $fillable = ['name', 'guard_name', 'description']; - - public function getDescriptionAttribute($value) - { - return ucfirst($value); - } -} - -// Custom Role Model -class CustomRole extends \Hypervel\Permission\Models\Role -{ - protected array $fillable = ['name', 'guard_name', 'description']; - - public function users() - { - return $this->morphedByMany(User::class, 'owner', 'owner_has_roles'); - } -} - -// Update config -'models' => [ - 'permission' => CustomPermission::class, - 'role' => CustomRole::class, -], -``` - -## Caching - -The package includes sophisticated caching to ensure high performance: - -### Cache Strategy - -- **All Roles with Permissions**: Cached globally to avoid N+1 queries when checking role-based permissions -- **Owner Roles**: Individual user/model roles are cached separately -- **Owner Permissions**: Individual user/model permissions are cached separately - -### Cache Invalidation - -Cache is automatically cleared when: -- Roles or permissions are assigned/revoked -- Role permissions are modified -- Models with roles/permissions are updated - -### Manual Cache Management - -```php -use Hypervel\Permission\PermissionManager; - -$manager = app(PermissionManager::class); - -// Clear all caches -$manager->clearAllRolesPermissionsCache(); - -// Clear specific owner cache -$manager->clearOwnerCache(User::class, $user->id); - -// Get cache store -$cache = $manager->getCache(); -``` - -## Performance Considerations - -### Optimizing Queries - -The package is designed to minimize database queries through caching: - -```php -// This loads and caches all roles with their permissions in one query -$allRolesWithPermissions = $manager->getAllRolesWithPermissions(); - -// Subsequent permission checks use cached data -$user->hasPermissionViaRoles('edit articles'); // No additional queries -``` - -### Eager Loading - -When working with multiple models, consider eager loading: - -```php -// Load users with their roles and permissions -$users = User::with(['roles.permissions', 'permissions'])->get(); - -// Now permission checks won't trigger additional queries -foreach ($users as $user) { - if ($user->hasPermission('edit articles')) { - // Process user - } -} -``` - -### Cache Warming - -For high-traffic applications, warm the cache on deployment: - -```php -use Hypervel\Permission\PermissionManager; - -// In a deployment script or command -$manager = app(PermissionManager::class); -$manager->getAllRolesWithPermissions(); // Warms the cache -``` - -## Common Patterns - -### Hierarchical Permissions - -```php -// Create a hierarchy using role inheritance -$admin = Role::create(['name' => 'admin', 'guard_name' => 'web']); -$editor = Role::create(['name' => 'editor', 'guard_name' => 'web']); -$writer = Role::create(['name' => 'writer', 'guard_name' => 'web']); - -// Assign cumulative permissions -$writer->givePermissionTo(['create articles', 'edit own articles']); -$editor->givePermissionTo(['create articles', 'edit own articles', 'edit all articles', 'publish articles']); - -// Users get permissions through role assignment -$user->assignRole('editor'); // Gets all editor permissions -``` - -### Resource-Based Permissions - -```php -// Create permissions for specific resources -Permission::create(['name' => 'view articles', 'guard_name' => 'web']); -Permission::create(['name' => 'create articles', 'guard_name' => 'web']); -Permission::create(['name' => 'edit articles', 'guard_name' => 'web']); -Permission::create(['name' => 'delete articles', 'guard_name' => 'web']); - -Permission::create(['name' => 'view users', 'guard_name' => 'web']); -Permission::create(['name' => 'create users', 'guard_name' => 'web']); -Permission::create(['name' => 'edit users', 'guard_name' => 'web']); -Permission::create(['name' => 'delete users', 'guard_name' => 'web']); -``` - -### Dynamic Permission Checking - -```php -class ArticleController extends Controller -{ - public function show(Article $article) - { - // Check if user can view this specific article - if (!$this->canView($article)) { - throw new UnauthorizedException('Cannot view this article'); - } - - return view('articles.show', compact('article')); - } - - private function canView(Article $article): bool - { - $user = auth()->user(); - - // Admin can view all - if ($user->hasRole('admin')) { - return true; - } - - // Users can view published articles - if ($article->is_published && $user->hasPermission('view articles')) { - return true; - } - - // Authors can view their own articles - if ($article->author_id === $user->id && $user->hasPermission('view own articles')) { - return true; - } - - return false; - } -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/processes.md b/_archive/docs/0.3/docs/processes.md deleted file mode 100644 index fa64a017d0..0000000000 --- a/_archive/docs/0.3/docs/processes.md +++ /dev/null @@ -1,556 +0,0 @@ -# Processes -[[toc]] - -## Introduction - -Hypervel provides an expressive, minimal API around the [Symfony Process component](https://symfony.com/doc/current/components/process.html), allowing you to conveniently invoke external processes from your Hypervel application. Hypervel's process features are focused on the most common use cases and a wonderful developer experience. - -## Invoking Processes - -To invoke a process, you may use the `run` and `start` methods offered by the `Process` facade. The `run` method will invoke a process and wait for the process to finish executing, while the `start` method is used for asynchronous process execution. We'll examine both approaches within this documentation. First, let's examine how to invoke a basic, synchronous process and inspect its result: - -```php -use Hypervel\Support\Facades\Process; - -$result = Process::run('ls -la'); - -return $result->output(); -``` - -Of course, the `Hypervel\Process\Contracts\ProcessResult` instance returned by the `run` method offers a variety of helpful methods that may be used to inspect the process result: - -```php -$result = Process::run('ls -la'); - -$result->successful(); -$result->failed(); -$result->exitCode(); -$result->output(); -$result->errorOutput(); -``` - -#### Throwing Exceptions - -If you have a process result and would like to throw an instance of `Hypervel\Process\Exceptions\ProcessFailedException` if the exit code is greater than zero (thus indicating failure), you may use the `throw` and `throwIf` methods. If the process did not fail, the process result instance will be returned: - -```php -$result = Process::run('ls -la')->throw(); - -$result = Process::run('ls -la')->throwIf($condition); -``` - -### Process Options - -Of course, you may need to customize the behavior of a process before invoking it. Thankfully, Hypervel allows you to tweak a variety of process features, such as the working directory, timeout, and environment variables. - -#### Working Directory Path - -You may use the `path` method to specify the working directory of the process. If this method is not invoked, the process will inherit the working directory of the currently executing PHP script: - -```php -$result = Process::path(__DIR__)->run('ls -la'); -``` - -#### Input - -You may provide input via the "standard input" of the process using the `input` method: - -```php -$result = Process::input('Hello World')->run('cat'); -``` - -#### Timeouts - -By default, processes will throw an instance of `Hypervel\Process\Exceptions\ProcessTimedOutException` after executing for more than 60 seconds. However, you can customize this behavior via the `timeout` method: - -```php -$result = Process::timeout(120)->run('bash import.sh'); -``` - -Or, if you would like to disable the process timeout entirely, you may invoke the `forever` method: - -```php -$result = Process::forever()->run('bash import.sh'); -``` - -The `idleTimeout` method may be used to specify the maximum number of seconds the process may run without returning any output: - -```php -$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh'); -``` - -#### Environment Variables - -Environment variables may be provided to the process via the `env` method. The invoked process will also inherit all of the environment variables defined by your system: - -```php -$result = Process::forever() - ->env(['IMPORT_PATH' => __DIR__]) - ->run('bash import.sh'); -``` - -If you wish to remove an inherited environment variable from the invoked process, you may provide that environment variable with a value of `false`: - -```php -$result = Process::forever() - ->env(['LOAD_PATH' => false]) - ->run('bash import.sh'); -``` - -#### TTY Mode - -The `tty` method may be used to enable TTY mode for your process. TTY mode connects the input and output of the process to the input and output of your program, allowing your process to open an editor like Vim or Nano as a process: - -```php -Process::forever()->tty()->run('vim'); -``` - -### Process Output - -As previously discussed, process output may be accessed using the `output` (stdout) and `errorOutput` (stderr) methods on a process result: - -```php -use Hypervel\Support\Facades\Process; - -$result = Process::run('ls -la'); - -echo $result->output(); -echo $result->errorOutput(); -``` - -However, output may also be gathered in real-time by passing a closure as the second argument to the `run` method. The closure will receive two arguments: the "type" of output (`stdout` or `stderr`) and the output string itself: - -```php -$result = Process::run('ls -la', function (string $type, string $output) { - echo $output; -}); -``` - -Hypervel also offers the `seeInOutput` and `seeInErrorOutput` methods, which provide a convenient way to determine if a given string was contained in the process' output: - -```php -if (Process::run('ls -la')->seeInOutput('Hypervel')) { - // ... -} -``` - -#### Disabling Process Output - -If your process is writing a significant amount of output that you are not interested in, you can conserve memory by disabling output retrieval entirely. To accomplish this, invoke the `quietly` method while building the process: - -```php -use Hypervel\Support\Facades\Process; - -$result = Process::quietly()->run('bash import.sh'); -``` - -### Pipelines - -Sometimes you may want to make the output of one process the input of another process. This is often referred to as "piping" the output of a process into another. The `pipe` method provided by the `Process` facades makes this easy to accomplish. The `pipe` method will execute the piped processes synchronously and return the process result for the last process in the pipeline: - -```php -use Hypervel\Process\Pipe; -use Hypervel\Support\Facades\Process; - -$result = Process::pipe(function (Pipe $pipe) { - $pipe->command('cat example.txt'); - $pipe->command('grep -i "laravel"'); -}); - -if ($result->successful()) { - // ... -} -``` - -If you do not need to customize the individual processes that make up the pipeline, you may simply pass an array of command strings to the `pipe` method: - -```php -$result = Process::pipe([ - 'cat example.txt', - 'grep -i "laravel"', -]); -``` - -The process output may be gathered in real-time by passing a closure as the second argument to the `pipe` method. The closure will receive two arguments: the "type" of output (`stdout` or `stderr`) and the output string itself: - -```php -$result = Process::pipe(function (Pipe $pipe) { - $pipe->command('cat example.txt'); - $pipe->command('grep -i "Hypervel"'); -}, function (string $type, string $output) { - echo $output; -}); -``` - -Hypervel also allows you to assign string keys to each process within a pipeline via the `as` method. This key will also be passed to the output closure provided to the `pipe` method, allowing you to determine which process the output belongs to: - -```php -$result = Process::pipe(function (Pipe $pipe) { - $pipe->as('first')->command('cat example.txt'); - $pipe->as('second')->command('grep -i "laravel"'); -})->start(function (string $type, string $output, string $key) { - // ... -}); -``` - -## Asynchronous Processes - -While the `run` method invokes processes synchronously, the `start` method may be used to invoke a process asynchronously. This allows your application to continue performing other tasks while the process runs in the background. Once the process has been invoked, you may utilize the `running` method to determine if the process is still running: - -```php -$process = Process::timeout(120)->start('bash import.sh'); - -while ($process->running()) { - // ... -} - -$result = $process->wait(); -``` - -As you may have noticed, you may invoke the `wait` method to wait until the process is finished executing and retrieve the process result instance: - -```php -$process = Process::timeout(120)->start('bash import.sh'); - -// ... - -$result = $process->wait(); -``` - -### Process IDs and Signals - -The `id` method may be used to retrieve the operating system assigned process ID of the running process: - -```php -$process = Process::start('bash import.sh'); - -return $process->id(); -``` - -You may use the `signal` method to send a "signal" to the running process. A list of predefined signal constants can be found within the [PHP documentation](https://www.php.net/manual/en/pcntl.constants.php): - -```php -$process->signal(SIGUSR2); -``` - -### Asynchronous Process Output - -While an asynchronous process is running, you may access its entire current output using the `output` and `errorOutput` methods; however, you may utilize the `latestOutput` and `latestErrorOutput` to access the output from the process that has occurred since the output was last retrieved: - -```php -$process = Process::timeout(120)->start('bash import.sh'); - -while ($process->running()) { - echo $process->latestOutput(); - echo $process->latestErrorOutput(); - - sleep(1); -} -``` - -Like the `run` method, output may also be gathered in real-time from asynchronous processes by passing a closure as the second argument to the `start` method. The closure will receive two arguments: the "type" of output (`stdout` or `stderr`) and the output string itself: - -```php -$process = Process::start('bash import.sh', function (string $type, string $output) { - echo $output; -}); - -$result = $process->wait(); -``` - -## Concurrent Processes - -Hypervel also makes it a breeze to manage a pool of concurrent, asynchronous processes, allowing you to easily execute many tasks simultaneously. To get started, invoke the `pool` method, which accepts a closure that receives an instance of `Hypervel\Process\Pool`. - -Within this closure, you may define the processes that belong to the pool. Once a process pool is started via the `start` method, you may access the [collection](/docs/collections) of running processes via the `running` method: - -```php -use Hypervel\Process\Pool; -use Hypervel\Support\Facades\Process; - -$pool = Process::pool(function (Pool $pool) { - $pool->path(__DIR__)->command('bash import-1.sh'); - $pool->path(__DIR__)->command('bash import-2.sh'); - $pool->path(__DIR__)->command('bash import-3.sh'); -})->start(function (string $type, string $output, int $key) { - // ... -}); - -while ($pool->running()->isNotEmpty()) { - // ... -} - -$results = $pool->wait(); -``` - -As you can see, you may wait for all of the pool processes to finish executing and resolve their results via the `wait` method. The `wait` method returns an array accessible object that allows you to access the process result instance of each process in the pool by its key: - -```php -$results = $pool->wait(); - -echo $results[0]->output(); -``` - -Or, for convenience, the `concurrently` method may be used to start an asynchronous process pool and immediately wait on its results. This can provide particularly expressive syntax when combined with PHP's array destructuring capabilities: - -```php -[$first, $second, $third] = Process::concurrently(function (Pool $pool) { - $pool->path(__DIR__)->command('ls -la'); - $pool->path(app_path())->command('ls -la'); - $pool->path(storage_path())->command('ls -la'); -}); - -echo $first->output(); -``` - -### Naming Pool Processes - -Accessing process pool results via a numeric key is not very expressive; therefore, Hypervel allows you to assign string keys to each process within a pool via the `as` method. This key will also be passed to the closure provided to the `start` method, allowing you to determine which process the output belongs to: - -```php -$pool = Process::pool(function (Pool $pool) { - $pool->as('first')->command('bash import-1.sh'); - $pool->as('second')->command('bash import-2.sh'); - $pool->as('third')->command('bash import-3.sh'); -})->start(function (string $type, string $output, string $key) { - // ... -}); - -$results = $pool->wait(); - -return $results['first']->output(); -``` - -### Pool Process IDs and Signals - -Since the process pool's `running` method provides a collection of all invoked processes within the pool, you may easily access the underlying pool process IDs: - -```php -$processIds = $pool->running()->each->id(); -``` - -And, for convenience, you may invoke the `signal` method on a process pool to send a signal to every process within the pool: - -```php -$pool->signal(SIGUSR2); -``` - -## Testing - -Many Hypervel services provide functionality to help you easily and expressively write tests, and Hypervel's process service is no exception. The `Process` facade's `fake` method allows you to instruct Hypervel to return stubbed / dummy results when processes are invoked. - -### Faking Processes - -To explore Hypervel's ability to fake processes, let's imagine a route that invokes a process: - -```php -use Hypervel\Support\Facades\Process; -use Hypervel\Support\Facades\Route; - -Route::get('/import', function () { - Process::run('bash import.sh'); - - return 'Import complete!'; -}); -``` - -When testing this route, we can instruct Hypervel to return a fake, successful process result for every invoked process by calling the `fake` method on the `Process` facade with no arguments. In addition, we can even [assert](#available-assertions) that a given process was "run": - -```php -get('/import'); - - // Simple process assertion... - Process::assertRan('bash import.sh'); - - // Or, inspecting the process configuration... - Process::assertRan(function (PendingProcess $process, ProcessResult $result) { - return $process->command === 'bash import.sh' && - $process->timeout === 60; - }); - } -} -``` - -As discussed, invoking the `fake` method on the `Process` facade will instruct Hypervel to always return a successful process result with no output. However, you may easily specify the output and exit code for faked processes using the `Process` facade's `result` method: - -```php -Process::fake([ - '*' => Process::result( - output: 'Test output', - errorOutput: 'Test error output', - exitCode: 1, - ), -]); -``` - -### Faking Specific Processes - -As you may have noticed in a previous example, the `Process` facade allows you to specify different fake results per process by passing an array to the `fake` method. - -The array's keys should represent command patterns that you wish to fake and their associated results. The `*` character may be used as a wildcard character. Any process commands that have not been faked will actually be invoked. You may use the `Process` facade's `result` method to construct stub / fake results for these commands: - -```php -Process::fake([ - 'cat *' => Process::result( - output: 'Test "cat" output', - ), - 'ls *' => Process::result( - output: 'Test "ls" output', - ), -]); -``` - -If you do not need to customize the exit code or error output of a faked process, you may find it more convenient to specify the fake process results as simple strings: - -```php -Process::fake([ - 'cat *' => 'Test "cat" output', - 'ls *' => 'Test "ls" output', -]); -``` - -### Faking Process Sequences - -If the code you are testing invokes multiple processes with the same command, you may wish to assign a different fake process result to each process invocation. You may accomplish this via the `Process` facade's `sequence` method: - -```php -Process::fake([ - 'ls *' => Process::sequence() - ->push(Process::result('First invocation')) - ->push(Process::result('Second invocation')), -]); -``` - -### Faking Asynchronous Process Lifecycles - -Thus far, we have primarily discussed faking processes which are invoked synchronously using the `run` method. However, if you are attempting to test code that interacts with asynchronous processes invoked via `start`, you may need a more sophisticated approach to describing your fake processes. - -For example, let's imagine the following route which interacts with an asynchronous process: - -```php -use Hypervel\Support\Facades\Log; -use Hypervel\Support\Facades\Route; - -Route::get('/import', function () { - $process = Process::start('bash import.sh'); - - while ($process->running()) { - Log::info($process->latestOutput()); - Log::info($process->latestErrorOutput()); - } - - return 'Done'; -}); -``` - -To properly fake this process, we need to be able to describe how many times the `running` method should return `true`. In addition, we may want to specify multiple lines of output that should be returned in sequence. To accomplish this, we can use the `Process` facade's `describe` method: - -```php -Process::fake([ - 'bash import.sh' => Process::describe() - ->output('First line of standard output') - ->errorOutput('First line of error output') - ->output('Second line of standard output') - ->exitCode(0) - ->iterations(3), -]); -``` - -Let's dig into the example above. Using the `output` and `errorOutput` methods, we may specify multiple lines of output that will be returned in sequence. The `exitCode` method may be used to specify the final exit code of the fake process. Finally, the `iterations` method may be used to specify how many times the `running` method should return `true`. - -### Available Assertions - -As [previously discussed](#faking-processes), Hypervel provides several process assertions for your feature tests. We'll discuss each of these assertions below. - -#### assertRan - -Assert that a given process was invoked: - -```php -use Hypervel\Support\Facades\Process; - -Process::assertRan('ls -la'); -``` - -The `assertRan` method also accepts a closure, which will receive an instance of a process and a process result, allowing you to inspect the process' configured options. If this closure returns `true`, the assertion will "pass": - -```php -Process::assertRan(fn ($process, $result) => - $process->command === 'ls -la' && - $process->path === __DIR__ && - $process->timeout === 60 -); -``` - -The `$process` passed to the `assertRan` closure is an instance of `Hypervel\Process\PendingProcess`, while the `$result` is an instance of `Hypervel\Process\Contracts\ProcessResult`. - -#### assertDidntRun - -Assert that a given process was not invoked: - -```php -use Hypervel\Support\Facades\Process; - -Process::assertDidntRun('ls -la'); -``` - -Like the `assertRan` method, the `assertDidntRun` method also accepts a closure, which will receive an instance of a process and a process result, allowing you to inspect the process' configured options. If this closure returns `true`, the assertion will "fail": - -```php -Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) => - $process->command === 'ls -la' -); -``` - -#### assertRanTimes - -Assert that a given process was invoked a given number of times: - -```php -use Hypervel\Support\Facades\Process; - -Process::assertRanTimes('ls -la', times: 3); -``` - -The `assertRanTimes` method also accepts a closure, which will receive an instance of a process and a process result, allowing you to inspect the process' configured options. If this closure returns `true` and the process was invoked the specified number of times, the assertion will "pass": - -```php -Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) { - return $process->command === 'ls -la'; -}, times: 3); -``` - -### Preventing Stray Processes - -If you would like to ensure that all invoked processes have been faked throughout your individual test or complete test suite, you can call the `preventStrayProcesses` method. After calling this method, any processes that do not have a corresponding fake result will throw an exception rather than starting an actual process: - -```php -use Hypervel\Support\Facades\Process; - -Process::preventStrayProcesses(); - -Process::fake([ - 'ls *' => 'Test output...', -]); - -// Fake response is returned... -Process::run('ls -la'); - -// An exception is thrown... -Process::run('bash import.sh'); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/prompts.md b/_archive/docs/0.3/docs/prompts.md deleted file mode 100644 index 8849293092..0000000000 --- a/_archive/docs/0.3/docs/prompts.md +++ /dev/null @@ -1,949 +0,0 @@ -# Prompts -[[toc]] - -## Introduction - -[Hypervel Prompts](https://github.com/hypervel/prompts) is a PHP package for adding beautiful and user-friendly forms to your command-line applications, with browser-like features including placeholder text and validation. - -![Prompts](/images/prompts-example.png) - -Hypervel Prompts is perfect for accepting user input in your [Artisan console commands](/docs/artisan#writing-commands), but it may also be used in any command-line PHP project. - -## Installation - -Hypervel Prompts is already included with the latest release of Hypervel. - -Hypervel Prompts may also be installed in your other PHP projects by using the Composer package manager: - -```shell -composer require hypervel/prompts -``` - -## Available Prompts - -### Text - -The `text` function will prompt the user with the given question, accept their input, and then return it: - -```php -use function Hypervel\Prompts\text; - -$name = text('What is your name?'); -``` - -You may also include placeholder text, a default value, and an informational hint: - -```php -$name = text( - label: 'What is your name?', - placeholder: 'E.g. Taylor Otwell', - default: $user?->name, - hint: 'This will be displayed on your profile.' -); -``` - -#### Required Values - -If you require a value to be entered, you may pass the `required` argument: - -```php -$name = text( - label: 'What is your name?', - required: true -); -``` - -If you would like to customize the validation message, you may also pass a string: - -```php -$name = text( - label: 'What is your name?', - required: 'Your name is required.' -); -``` - -#### Additional Validation - -Finally, if you would like to perform additional validation logic, you may pass a closure to the `validate` argument: - -```php -$name = text( - label: 'What is your name?', - validate: fn (string $value) => match (true) { - strlen($value) < 3 => 'The name must be at least 3 characters.', - strlen($value) > 255 => 'The name must not exceed 255 characters.', - default => null - } -); -``` - -The closure will receive the value that has been entered and may return an error message, or `null` if the validation passes. - -Alternatively, you may leverage the power of Hypervel's [validator](/docs/validation). To do so, provide an array containing the name of the attribute and the desired validation rules to the `validate` argument: - -```php -$name = text( - label: 'What is your name?', - validate: ['name' => 'required|max:255|unique:users'] -); -``` - -### Textarea - -The `textarea` function will prompt the user with the given question, accept their input via a multi-line textarea, and then return it: - -```php -use function Hypervel\Prompts\textarea; - -$story = textarea('Tell me a story.'); -``` - -You may also include placeholder text, a default value, and an informational hint: - -```php -$story = textarea( - label: 'Tell me a story.', - placeholder: 'This is a story about...', - hint: 'This will be displayed on your profile.' -); -``` - -#### Required Values - -If you require a value to be entered, you may pass the `required` argument: - -```php -$story = textarea( - label: 'Tell me a story.', - required: true -); -``` - -If you would like to customize the validation message, you may also pass a string: - -```php -$story = textarea( - label: 'Tell me a story.', - required: 'A story is required.' -); -``` - -#### Additional Validation - -Finally, if you would like to perform additional validation logic, you may pass a closure to the `validate` argument: - -```php -$story = textarea( - label: 'Tell me a story.', - validate: fn (string $value) => match (true) { - strlen($value) < 250 => 'The story must be at least 250 characters.', - strlen($value) > 10000 => 'The story must not exceed 10,000 characters.', - default => null - } -); -``` - -The closure will receive the value that has been entered and may return an error message, or `null` if the validation passes. - -Alternatively, you may leverage the power of Hypervel's [validator](/docs/validation). To do so, provide an array containing the name of the attribute and the desired validation rules to the `validate` argument: - -```php -$story = textarea( - label: 'Tell me a story.', - validate: ['story' => 'required|max:10000'] -); -``` - -### Password - -The `password` function is similar to the `text` function, but the user's input will be masked as they type in the console. This is useful when asking for sensitive information such as passwords: - -```php -use function Hypervel\Prompts\password; - -$password = password('What is your password?'); -``` - -You may also include placeholder text and an informational hint: - -```php -$password = password( - label: 'What is your password?', - placeholder: 'password', - hint: 'Minimum 8 characters.' -); -``` - -#### Required Values - -If you require a value to be entered, you may pass the `required` argument: - -```php -$password = password( - label: 'What is your password?', - required: true -); -``` - -If you would like to customize the validation message, you may also pass a string: - -```php -$password = password( - label: 'What is your password?', - required: 'The password is required.' -); -``` - -#### Additional Validation - -Finally, if you would like to perform additional validation logic, you may pass a closure to the `validate` argument: - -```php -$password = password( - label: 'What is your password?', - validate: fn (string $value) => match (true) { - strlen($value) < 8 => 'The password must be at least 8 characters.', - default => null - } -); -``` - -The closure will receive the value that has been entered and may return an error message, or `null` if the validation passes. - -Alternatively, you may leverage the power of Hypervel's [validator](/docs/validation). To do so, provide an array containing the name of the attribute and the desired validation rules to the `validate` argument: - -```php -$password = password( - label: 'What is your password?', - validate: ['password' => 'min:8'] -); -``` - -### Confirm - -If you need to ask the user for a "yes or no" confirmation, you may use the `confirm` function. Users may use the arrow keys or press `y` or `n` to select their response. This function will return either `true` or `false`. - -```php -use function Hypervel\Prompts\confirm; - -$confirmed = confirm('Do you accept the terms?'); -``` - -You may also include a default value, customized wording for the "Yes" and "No" labels, and an informational hint: - -```php -$confirmed = confirm( - label: 'Do you accept the terms?', - default: false, - yes: 'I accept', - no: 'I decline', - hint: 'The terms must be accepted to continue.' -); -``` - -#### Requiring "Yes" - -If necessary, you may require your users to select "Yes" by passing the `required` argument: - -```php -$confirmed = confirm( - label: 'Do you accept the terms?', - required: true -); -``` - -If you would like to customize the validation message, you may also pass a string: - -```php -$confirmed = confirm( - label: 'Do you accept the terms?', - required: 'You must accept the terms to continue.' -); -``` - -### Select - -If you need the user to select from a predefined set of choices, you may use the `select` function: - -```php -use function Hypervel\Prompts\select; - -$role = select( - label: 'What role should the user have?', - options: ['Member', 'Contributor', 'Owner'] -); -``` - -You may also specify the default choice and an informational hint: - -```php -$role = select( - label: 'What role should the user have?', - options: ['Member', 'Contributor', 'Owner'], - default: 'Owner', - hint: 'The role may be changed at any time.' -); -``` - -You may also pass an associative array to the `options` argument to have the selected key returned instead of its value: - -```php -$role = select( - label: 'What role should the user have?', - options: [ - 'member' => 'Member', - 'contributor' => 'Contributor', - 'owner' => 'Owner', - ], - default: 'owner' -); -``` - -Up to five options will be displayed before the list begins to scroll. You may customize this by passing the `scroll` argument: - -```php -$role = select( - label: 'Which category would you like to assign?', - options: Category::pluck('name', 'id'), - scroll: 10 -); -``` - -#### Additional Validation - -Unlike other prompt functions, the `select` function doesn't accept the `required` argument because it is not possible to select nothing. However, you may pass a closure to the `validate` argument if you need to present an option but prevent it from being selected: - -```php -$role = select( - label: 'What role should the user have?', - options: [ - 'member' => 'Member', - 'contributor' => 'Contributor', - 'owner' => 'Owner', - ], - validate: fn (string $value) => - $value === 'owner' && User::where('role', 'owner')->exists() - ? 'An owner already exists.' - : null -); -``` - -If the `options` argument is an associative array, then the closure will receive the selected key, otherwise it will receive the selected value. The closure may return an error message, or `null` if the validation passes. - -### Multi-select - -If you need the user to be able to select multiple options, you may use the `multiselect` function: - -```php -use function Hypervel\Prompts\multiselect; - -$permissions = multiselect( - label: 'What permissions should be assigned?', - options: ['Read', 'Create', 'Update', 'Delete'] -); -``` - -You may also specify default choices and an informational hint: - -```php -use function Hypervel\Prompts\multiselect; - -$permissions = multiselect( - label: 'What permissions should be assigned?', - options: ['Read', 'Create', 'Update', 'Delete'], - default: ['Read', 'Create'], - hint: 'Permissions may be updated at any time.' -); -``` - -You may also pass an associative array to the `options` argument to return the selected options' keys instead of their values: - -```php -$permissions = multiselect( - label: 'What permissions should be assigned?', - options: [ - 'read' => 'Read', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - ], - default: ['read', 'create'] -); -``` - -Up to five options will be displayed before the list begins to scroll. You may customize this by passing the `scroll` argument: - -```php -$categories = multiselect( - label: 'What categories should be assigned?', - options: Category::pluck('name', 'id'), - scroll: 10 -); -``` - -#### Requiring a Value - -By default, the user may select zero or more options. You may pass the `required` argument to enforce one or more options instead: - -```php -$categories = multiselect( - label: 'What categories should be assigned?', - options: Category::pluck('name', 'id'), - required: true -); -``` - -If you would like to customize the validation message, you may provide a string to the `required` argument: - -```php -$categories = multiselect( - label: 'What categories should be assigned?', - options: Category::pluck('name', 'id'), - required: 'You must select at least one category' -); -``` - -#### Additional Validation - -You may pass a closure to the `validate` argument if you need to present an option but prevent it from being selected: - -```php -$permissions = multiselect( - label: 'What permissions should the user have?', - options: [ - 'read' => 'Read', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - ], - validate: fn (array $values) => ! in_array('read', $values) - ? 'All users require the read permission.' - : null -); -``` - -If the `options` argument is an associative array then the closure will receive the selected keys, otherwise it will receive the selected values. The closure may return an error message, or `null` if the validation passes. - -### Suggest - -The `suggest` function can be used to provide auto-completion for possible choices. The user can still provide any answer, regardless of the auto-completion hints: - -```php -use function Hypervel\Prompts\suggest; - -$name = suggest('What is your name?', ['Taylor', 'Dayle']); -``` - -Alternatively, you may pass a closure as the second argument to the `suggest` function. The closure will be called each time the user types an input character. The closure should accept a string parameter containing the user's input so far and return an array of options for auto-completion: - -```php -$name = suggest( - label: 'What is your name?', - options: fn ($value) => collect(['Taylor', 'Dayle']) - ->filter(fn ($name) => Str::contains($name, $value, ignoreCase: true)) -) -``` - -You may also include placeholder text, a default value, and an informational hint: - -```php -$name = suggest( - label: 'What is your name?', - options: ['Taylor', 'Dayle'], - placeholder: 'E.g. Taylor', - default: $user?->name, - hint: 'This will be displayed on your profile.' -); -``` - -#### Required Values - -If you require a value to be entered, you may pass the `required` argument: - -```php -$name = suggest( - label: 'What is your name?', - options: ['Taylor', 'Dayle'], - required: true -); -``` - -If you would like to customize the validation message, you may also pass a string: - -```php -$name = suggest( - label: 'What is your name?', - options: ['Taylor', 'Dayle'], - required: 'Your name is required.' -); -``` - -#### Additional Validation - -Finally, if you would like to perform additional validation logic, you may pass a closure to the `validate` argument: - -```php -$name = suggest( - label: 'What is your name?', - options: ['Taylor', 'Dayle'], - validate: fn (string $value) => match (true) { - strlen($value) < 3 => 'The name must be at least 3 characters.', - strlen($value) > 255 => 'The name must not exceed 255 characters.', - default => null - } -); -``` - -The closure will receive the value that has been entered and may return an error message, or `null` if the validation passes. - -Alternatively, you may leverage the power of Hypervel's [validator](/docs/validation). To do so, provide an array containing the name of the attribute and the desired validation rules to the `validate` argument: - -```php -$name = suggest( - label: 'What is your name?', - options: ['Taylor', 'Dayle'], - validate: ['name' => 'required|min:3|max:255'] -); -``` - -### Search - -If you have a lot of options for the user to select from, the `search` function allows the user to type a search query to filter the results before using the arrow keys to select an option: - -```php -use function Hypervel\Prompts\search; - -$id = search( - label: 'Search for the user that should receive the mail', - options: fn (string $value) => strlen($value) > 0 - ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() - : [] -); -``` - -The closure will receive the text that has been typed by the user so far and must return an array of options. If you return an associative array then the selected option's key will be returned, otherwise its value will be returned instead. - -When filtering an array where you intend to return the value, you should use the `array_values` function or the `values` Collection method to ensure the array doesn't become associative: - -```php -$names = collect(['Taylor', 'Abigail']); - -$selected = search( - label: 'Search for the user that should receive the mail', - options: fn (string $value) => $names - ->filter(fn ($name) => Str::contains($name, $value, ignoreCase: true)) - ->values() - ->all(), -); -``` - -You may also include placeholder text and an informational hint: - -```php -$id = search( - label: 'Search for the user that should receive the mail', - placeholder: 'E.g. Taylor Otwell', - options: fn (string $value) => strlen($value) > 0 - ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() - : [], - hint: 'The user will receive an email immediately.' -); -``` - -Up to five options will be displayed before the list begins to scroll. You may customize this by passing the `scroll` argument: - -```php -$id = search( - label: 'Search for the user that should receive the mail', - options: fn (string $value) => strlen($value) > 0 - ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() - : [], - scroll: 10 -); -``` - -#### Additional Validation - -If you would like to perform additional validation logic, you may pass a closure to the `validate` argument: - -```php -$id = search( - label: 'Search for the user that should receive the mail', - options: fn (string $value) => strlen($value) > 0 - ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() - : [], - validate: function (int|string $value) { - $user = User::findOrFail($value); - - if ($user->opted_out) { - return 'This user has opted-out of receiving mail.'; - } - } -); -``` - -If the `options` closure returns an associative array, then the closure will receive the selected key, otherwise, it will receive the selected value. The closure may return an error message, or `null` if the validation passes. - -### Multi-search - -If you have a lot of searchable options and need the user to be able to select multiple items, the `multisearch` function allows the user to type a search query to filter the results before using the arrow keys and space-bar to select options: - -```php -use function Hypervel\Prompts\multisearch; - -$ids = multisearch( - 'Search for the users that should receive the mail', - fn (string $value) => strlen($value) > 0 - ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() - : [] -); -``` - -The closure will receive the text that has been typed by the user so far and must return an array of options. If you return an associative array then the selected options' keys will be returned; otherwise, their values will be returned instead. - -When filtering an array where you intend to return the value, you should use the `array_values` function or the `values` Collection method to ensure the array doesn't become associative: - -```php -$names = collect(['Taylor', 'Abigail']); - -$selected = multisearch( - label: 'Search for the users that should receive the mail', - options: fn (string $value) => $names - ->filter(fn ($name) => Str::contains($name, $value, ignoreCase: true)) - ->values() - ->all(), -); -``` - -You may also include placeholder text and an informational hint: - -```php -$ids = multisearch( - label: 'Search for the users that should receive the mail', - placeholder: 'E.g. Taylor Otwell', - options: fn (string $value) => strlen($value) > 0 - ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() - : [], - hint: 'The user will receive an email immediately.' -); -``` - -Up to five options will be displayed before the list begins to scroll. You may customize this by providing the `scroll` argument: - -```php -$ids = multisearch( - label: 'Search for the users that should receive the mail', - options: fn (string $value) => strlen($value) > 0 - ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() - : [], - scroll: 10 -); -``` - -#### Requiring a Value - -By default, the user may select zero or more options. You may pass the `required` argument to enforce one or more options instead: - -```php -$ids = multisearch( - label: 'Search for the users that should receive the mail', - options: fn (string $value) => strlen($value) > 0 - ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() - : [], - required: true -); -``` - -If you would like to customize the validation message, you may also provide a string to the `required` argument: - -```php -$ids = multisearch( - label: 'Search for the users that should receive the mail', - options: fn (string $value) => strlen($value) > 0 - ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() - : [], - required: 'You must select at least one user.' -); -``` - -#### Additional Validation - -If you would like to perform additional validation logic, you may pass a closure to the `validate` argument: - -```php -$ids = multisearch( - label: 'Search for the users that should receive the mail', - options: fn (string $value) => strlen($value) > 0 - ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all() - : [], - validate: function (array $values) { - $optedOut = User::whereLike('name', '%a%')->findMany($values); - - if ($optedOut->isNotEmpty()) { - return $optedOut->pluck('name')->join(', ', ', and ').' have opted out.'; - } - } -); -``` - -If the `options` closure returns an associative array, then the closure will receive the selected keys; otherwise, it will receive the selected values. The closure may return an error message, or `null` if the validation passes. - -### Pause - -The `pause` function may be used to display informational text to the user and wait for them to confirm their desire to proceed by pressing the Enter / Return key: - -```php -use function Hypervel\Prompts\pause; - -pause('Press ENTER to continue.'); -``` - -## Transforming Input Before Validation - -Sometimes you may want to transform the prompt input before validation takes place. For example, you may wish to remove white space from any provided strings. To accomplish this, many of the prompt functions provide a `transform` argument, which accepts a closure: - -```php -$name = text( - label: 'What is your name?', - transform: fn (string $value) => trim($value), - validate: fn (string $value) => match (true) { - strlen($value) < 3 => 'The name must be at least 3 characters.', - strlen($value) > 255 => 'The name must not exceed 255 characters.', - default => null - } -); -``` - -## Forms - -Often, you will have multiple prompts that will be displayed in sequence to collect information before performing additional actions. You may use the `form` function to create a grouped set of prompts for the user to complete: - -```php -use function Hypervel\Prompts\form; - -$responses = form() - ->text('What is your name?', required: true) - ->password('What is your password?', validate: ['password' => 'min:8']) - ->confirm('Do you accept the terms?') - ->submit(); -``` - -The `submit` method will return a numerically indexed array containing all of the responses from the form's prompts. However, you may provide a name for each prompt via the `name` argument. When a name is provided, the named prompt's response may be accessed via that name: - -```php -use App\Models\User; -use function Hypervel\Prompts\form; - -$responses = form() - ->text('What is your name?', required: true, name: 'name') - ->password( - label: 'What is your password?', - validate: ['password' => 'min:8'], - name: 'password' - ) - ->confirm('Do you accept the terms?') - ->submit(); - -User::create([ - 'name' => $responses['name'], - 'password' => $responses['password'], -]); -``` - -The primary benefit of using the `form` function is the ability for the user to return to previous prompts in the form using `CTRL + U`. This allows the user to fix mistakes or alter selections without needing to cancel and restart the entire form. - -If you need more granular control over a prompt in a form, you may invoke the `add` method instead of calling one of the prompt functions directly. The `add` method is passed all previous responses provided by the user: - -```php -use function Hypervel\Prompts\form; -use function Hypervel\Prompts\outro; -use function Hypervel\Prompts\text; - -$responses = form() - ->text('What is your name?', required: true, name: 'name') - ->add(function ($responses) { - return text("How old are you, {$responses['name']}?"); - }, name: 'age') - ->submit(); - -outro("Your name is {$responses['name']} and you are {$responses['age']} years old."); -``` - -## Informational Messages - -The `note`, `info`, `warning`, `error`, and `alert` functions may be used to display informational messages: - -```php -use function Hypervel\Prompts\info; - -info('Package installed successfully.'); -``` - -## Tables - -The `table` function makes it easy to display multiple rows and columns of data. All you need to do is provide the column names and the data for the table: - -```php -use function Hypervel\Prompts\table; - -table( - headers: ['Name', 'Email'], - rows: User::all(['name', 'email'])->toArray() -); -``` - -## Spin - -The `spin` function displays a spinner along with an optional message while executing a specified callback. It serves to indicate ongoing processes and returns the callback's results upon completion: - -```php -use function Hypervel\Prompts\spin; - -$response = spin( - callback: fn () => Http::get('http://example.com'), - message: 'Fetching response...' -); -``` - -## Progress Bars - -For long running tasks, it can be helpful to show a progress bar that informs users how complete the task is. Using the `progress` function, Hypervel will display a progress bar and advance its progress for each iteration over a given iterable value: - -```php -use function Hypervel\Prompts\progress; - -$users = progress( - label: 'Updating users', - steps: User::all(), - callback: fn ($user) => $this->performTask($user) -); -``` - -The `progress` function acts like a map function and will return an array containing the return value of each iteration of your callback. - -The callback may also accept the `Hypervel\Prompts\Progress` instance, allowing you to modify the label and hint on each iteration: - -```php -$users = progress( - label: 'Updating users', - steps: User::all(), - callback: function ($user, $progress) { - $progress - ->label("Updating {$user->name}") - ->hint("Created on {$user->created_at}"); - - return $this->performTask($user); - }, - hint: 'This may take some time.' -); -``` - -Sometimes, you may need more manual control over how a progress bar is advanced. First, define the total number of steps the process will iterate through. Then, advance the progress bar via the `advance` method after processing each item: - -```php -$progress = progress(label: 'Updating users', steps: 10); - -$users = User::all(); - -$progress->start(); - -foreach ($users as $user) { - $this->performTask($user); - - $progress->advance(); -} - -$progress->finish(); -``` - -## Clearing the Terminal - -The `clear` function may be used to clear the user's terminal: - -```php -use function Hypervel\Prompts\clear; - -clear(); -``` - -## Terminal Considerations - -#### Terminal Width - -If the length of any label, option, or validation message exceeds the number of "columns" in the user's terminal, it will be automatically truncated to fit. Consider minimizing the length of these strings if your users may be using narrower terminals. A typically safe maximum length is 74 characters to support an 80-character terminal. - -#### Terminal Height - -For any prompts that accept the `scroll` argument, the configured value will automatically be reduced to fit the height of the user's terminal, including space for a validation message. - -#### Fallback Conditions - -If you are not using Hypervel or need to customize when the fallback behavior is used, you may pass a boolean to the `fallbackWhen` static method on the `Prompt` class: - -```php -use Hypervel\Prompts\Prompt; - -Prompt::fallbackWhen( - ! $input->isInteractive() || windows_os() || app()->runningUnitTests() -); -``` - -#### Fallback Behavior - -If you are not using Hypervel or need to customize the fallback behavior, you may pass a closure to the `fallbackUsing` static method on each prompt class: - -```php -use Hypervel\Prompts\TextPrompt; -use Symfony\Component\Console\Question\Question; -use Symfony\Component\Console\Style\SymfonyStyle; - -TextPrompt::fallbackUsing(function (TextPrompt $prompt) use ($input, $output) { - $question = (new Question($prompt->label, $prompt->default ?: null)) - ->setValidator(function ($answer) use ($prompt) { - if ($prompt->required && $answer === null) { - throw new \RuntimeException( - is_string($prompt->required) ? $prompt->required : 'Required.' - ); - } - - if ($prompt->validate) { - $error = ($prompt->validate)($answer ?? ''); - - if ($error) { - throw new \RuntimeException($error); - } - } - - return $answer; - }); - - return (new SymfonyStyle($input, $output)) - ->askQuestion($question); -}); -``` - -Fallbacks must be configured individually for each prompt class. The closure will receive an instance of the prompt class and must return an appropriate type for the prompt. - -## Testing - -Hypervel provides a variety of methods for testing that your command displays the expected Prompt messages: - -```php -public function testReportGeneration(): void -{ - $this->artisan('report:generate') - ->expectsPromptsInfo('Welcome to the application!') - ->expectsPromptsWarning('This action cannot be undone') - ->expectsPromptsError('Something went wrong') - ->expectsPromptsAlert('Important notice!') - ->expectsPromptsIntro('Starting process...') - ->expectsPromptsOutro('Process completed!') - ->expectsPrompexpectsPromptsWarningtsTable( - headers: ['Name', 'Email'], - rows: [ - ['Taylor Otwell', 'taylor@example.com'], - ['Jason Beggs', 'jason@example.com'], - ] - )->assertExitCode(0); -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/providers.md b/_archive/docs/0.3/docs/providers.md deleted file mode 100644 index a4602238b6..0000000000 --- a/_archive/docs/0.3/docs/providers.md +++ /dev/null @@ -1,187 +0,0 @@ -# Service Providers -[[toc]] - -## Introduction - -Service providers are the central place of all Hypervel application bootstrapping. Your own application, as well as all of Laravel's core services, are bootstrapped via service providers. - -Hypervel ensures that all components are properly configured and ready for use as soon as the application starts. This approach allows for a modular and flexible configuration system, where each component can manage its own configuration independently. - -But, what do we mean by "bootstrapped"? In general, we mean **registering** things, including registering service container bindings, event listeners, middleware, and even routes. Service providers are the central place to configure your application. - -If you open the `config/app.php` file included with Hypervel, you will see a providers array. These are all of the service provider classes that will be loaded for your application. By default, a set of Hypervel core service providers are listed in this array. - -In this overview, you will learn how to write your own service providers and register them with your Hypervel application. - -::: note -If you would like to learn more about how Hypervel handles requests and works internally, check out our documentation on the Laravel [request lifecycle](/docs/lifecycle). -::: - -## Writing Service Providers - -All service providers extend the `Hypervel\Support\ServiceProvider` class. Most service providers contain a `register` and a `boot` method. Within the `register` method, you should **only bind things into the [service container](/docs/container)**. You should never attempt to register any event listeners, routes, or any other piece of functionality within the `register` method. - -The Artisan CLI can generate a new provider via the `make:provider` command. - -```shell:no-line-numbers -php artisan make:provider RiakServiceProvider -``` - -### The Register Method - -As mentioned previously, within the `register` method, you should only bind things into the [service container](/docs/container). You should never attempt to register any event listeners, routes, or any other piece of functionality within the `register` method. Otherwise, you may accidentally use a service that is provided by a service provider which has not loaded yet. - -Let's take a look at a basic service provider. Within any of your service provider methods, you always have access to the `$app` property which provides access to the service container: - -```php -app->bind(Connection::class, function (Application $app) { - return new Connection(config('riak')); - }); - } -} -``` - -This service provider only defines a `register` method, and uses that method to define an implementation of `App\Services\Riak\Connection` in the service container. If you're not yet familiar with Hypervel's service container, check out [its documentation](/docs/container). - -#### The `bindings` Properties - -If your service provider registers many simple bindings, you may wish to use the `bindings` property instead of manually registering each container binding. When the service provider is loaded by the framework, it will automatically check for these properties and register their bindings: - -```php - DigitalOceanServerProvider::class, - ]; -} -``` - -:::tip -You can also set these bindings in `config/dependencies.php`. -::: - -### The Boot Method - -So, what if we need to extend `Request` functions within our service provider? This should be done within the `boot` method. **This method is called after all other service providers have been registered**, meaning you have access to all other services that have been registered by the framework: - -```php -setDefaultConnection('mysql'); -} -``` - -### Writing Provider Config - -Some behaviors must be achieved in config provider, such as class map replacement or aspect registration. Service providers can also provide the same functionality, you just need to implement `getProviderConfig` in the service provider: - -```php -public static function getProviderConfig(): array -{ - return [ - 'dependencies' => [], - 'annotations' => [ - 'scan' => [ - 'paths' => [ - __DIR__, - ], - ], - ], - 'listeners' => [], - 'publish' => [ - [ - 'id' => 'config', - 'description' => 'description of this config file.', - 'source' => __DIR__ . '/../publish/file.php', - 'destination' => BASE_PATH . '/config/autoload/file.php', - ], - ], - ]; -} -``` - -Then you need to configure your service provider in `composer.json` for auto discovery: - -```json -"extra": { - "branch-alias": { - "dev-main": "0.3-dev" - }, - "hypervel": { - "providers": [ - "Hypervel\\Sanctum\\SanctumServiceProvider" - ] - } -} -``` - -## Registering Providers - -All service providers are registered in the `config/app.php` configuration file. This file contains a `providers` array where you can list the class names of your service providers. By default, a set of Hypervel core service providers are registered in this array. The default providers bootstrap the core Hypervel components. - -To register your provider, add it to the array: - -```php -'providers' => [ - Hypervel\Foundation\Providers\FoundationServiceProvider::class, - App\Providers\RouteServiceProvider::class, - App\Providers\AppServiceProvider::class, - App\Providers\EventServiceProvider::class, -] -``` diff --git a/_archive/docs/0.3/docs/queries.md b/_archive/docs/0.3/docs/queries.md deleted file mode 100644 index 50dff8072e..0000000000 --- a/_archive/docs/0.3/docs/queries.md +++ /dev/null @@ -1,1282 +0,0 @@ -# Database: Query Builder -[[toc]] - -## Introduction - -Hypervel's database query builder provides a convenient, fluent interface to creating and running database queries. It can be used to perform most database operations in your application and works perfectly with all of Hypervel's supported database systems. - -The Hypervel query builder uses PDO parameter binding to protect your application against SQL injection attacks. There is no need to clean or sanitize strings passed to the query builder as query bindings. - -::: warning -PDO does not support binding column names. Therefore, you should never allow user input to dictate the column names referenced by your queries, including "order by" columns. -::: - -## Running Database Queries - -#### Retrieving All Rows From a Table - -You may use the `table` method provided by the `DB` facade to begin a query. The `table` method returns a fluent query builder instance for the given table, allowing you to chain more constraints onto the query and then finally retrieve the results of the query using the `get` method: - -```php -get(); - - return view('user.index', ['users' => $users]); - } -} -``` - -The `get` method returns an `Hypervel\Support\Collection` instance containing the results of the query where each result is an instance of the PHP `stdClass` object. You may access each column's value by accessing the column as a property of the object: - -```php -use Hypervel\Support\Facades\DB; - -$users = DB::table('users')->get(); - -foreach ($users as $user) { - echo $user->name; -} -``` - -::: note -Hypervel collections provide a variety of extremely powerful methods for mapping and reducing data. For more information on Hypervel collections, check out the [collection documentation](/docs/collections). -::: - -#### Retrieving a Single Row / Column From a Table - -If you just need to retrieve a single row from a database table, you may use the `DB` facade's `first` method. This method will return a single `stdClass` object: - -```php -$user = DB::table('users')->where('name', 'John')->first(); - -return $user->email; -``` - -If you would like to retrieve a single row from a database table, but throw an `Hyperf\Database\Model\ModelNotFoundException` if no matching row is found, you may use the `firstOrFail` method. If the `ModelNotFoundException` is not caught, a 404 HTTP response is automatically sent back to the client: - -```php -$user = DB::table('users')->where('name', 'John')->firstOrFail(); -``` - -If you don't need an entire row, you may extract a single value from a record using the `value` method. This method will return the value of the column directly: - -```php -$email = DB::table('users')->where('name', 'John')->value('email'); -``` - -To retrieve a single row by its `id` column value, use the `find` method: - -```php -$user = DB::table('users')->find(3); -``` - -#### Retrieving a List of Column Values - -If you would like to retrieve an `Hypervel\Support\Collection` instance containing the values of a single column, you may use the `pluck` method. In this example, we'll retrieve a collection of user titles: - -```php -use Hypervel\Support\Facades\DB; - -$titles = DB::table('users')->pluck('title'); - -foreach ($titles as $title) { - echo $title; -} -``` - -You may specify the column that the resulting collection should use as its keys by providing a second argument to the `pluck` method: - -```php -$titles = DB::table('users')->pluck('title', 'name'); - -foreach ($titles as $name => $title) { - echo $title; -} -``` - -### Chunking Results - -If you need to work with thousands of database records, consider using the `chunk` method provided by the `DB` facade. This method retrieves a small chunk of results at a time and feeds each chunk into a closure for processing. For example, let's retrieve the entire `users` table in chunks of 100 records at a time: - -```php -use Hypervel\Support\Collection; -use Hypervel\Support\Facades\DB; - -DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) { - foreach ($users as $user) { - // ... - } -}); -``` - -You may stop further chunks from being processed by returning `false` from the closure: - -```php -DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) { - // Process the records... - - return false; -}); -``` - -If you are updating database records while chunking results, your chunk results could change in unexpected ways. If you plan to update the retrieved records while chunking, it is always best to use the `chunkById` method instead. This method will automatically paginate the results based on the record's primary key: - -```php -DB::table('users')->where('active', false) - ->chunkById(100, function (Collection $users) { - foreach ($users as $user) { - DB::table('users') - ->where('id', $user->id) - ->update(['active' => true]); - } - }); -``` - -Since the `chunkById` and `lazyById` methods add their own "where" conditions to the query being executed, you should typically [logically group](#logical-grouping) your own conditions within a closure: - -```php -DB::table('users')->where(function ($query) { - $query->where('credits', 1)->orWhere('credits', 2); -})->chunkById(100, function (Collection $users) { - foreach ($users as $user) { - DB::table('users') - ->where('id', $user->id) - ->update(['credits' => 3]); - } -}); -``` - -::: warning -When updating or deleting records inside the chunk callback, any changes to the primary key or foreign keys could affect the chunk query. This could potentially result in records not being included in the chunked results. -::: - -### Streaming Results Lazily - -The `lazy` method works similarly to [the `chunk` method](#chunking-results) in the sense that it executes the query in chunks. However, instead of passing each chunk into a callback, the `lazy()` method returns a [`LazyCollection`](/docs/collections#lazy-collections), which lets you interact with the results as a single stream: - -```php -use Hypervel\Support\Facades\DB; - -DB::table('users')->orderBy('id')->lazy()->each(function (object $user) { - // ... -}); -``` - -Once again, if you plan to update the retrieved records while iterating over them, it is best to use the `lazyById` or `lazyByIdDesc` methods instead. These methods will automatically paginate the results based on the record's primary key: - -```php -DB::table('users')->where('active', false) - ->lazyById()->each(function (object $user) { - DB::table('users') - ->where('id', $user->id) - ->update(['active' => true]); - }); -``` - -::: warning -When updating or deleting records while iterating over them, any changes to the primary key or foreign keys could affect the chunk query. This could potentially result in records not being included in the results. -::: - -### Aggregates - -The query builder also provides a variety of methods for retrieving aggregate values like `count`, `max`, `min`, `avg`, and `sum`. You may call any of these methods after constructing your query: - -```php -use Hypervel\Support\Facades\DB; - -$users = DB::table('users')->count(); - -$price = DB::table('orders')->max('price'); -``` - -Of course, you may combine these methods with other clauses to fine-tune how your aggregate value is calculated: - -```php -$price = DB::table('orders') - ->where('finalized', 1) - ->avg('price'); -``` - -#### Determining if Records Exist - -Instead of using the `count` method to determine if any records exist that match your query's constraints, you may use the `exists` and `doesntExist` methods: - -```php -if (DB::table('orders')->where('finalized', 1)->exists()) { - // ... -} - -if (DB::table('orders')->where('finalized', 1)->doesntExist()) { - // ... -} -``` - -## Select Statements - -#### Specifying a Select Clause - -You may not always want to select all columns from a database table. Using the `select` method, you can specify a custom "select" clause for the query: - -```php -use Hypervel\Support\Facades\DB; - -$users = DB::table('users') - ->select('name', 'email as user_email') - ->get(); -``` - -The `distinct` method allows you to force the query to return distinct results: - -```php -$users = DB::table('users')->distinct()->get(); -``` - -If you already have a query builder instance and you wish to add a column to its existing select clause, you may use the `addSelect` method: - -```php -$query = DB::table('users')->select('name'); - -$users = $query->addSelect('age')->get(); -``` - -## Raw Expressions - -Sometimes you may need to insert an arbitrary string into a query. To create a raw string expression, you may use the `raw` method provided by the `DB` facade: - -```php -$users = DB::table('users') - ->select(DB::raw('count(*) as user_count, status')) - ->where('status', '<>', 1) - ->groupBy('status') - ->get(); -``` - -::: warning -Raw statements will be injected into the query as strings, so you should be extremely careful to avoid creating SQL injection vulnerabilities. -::: - -### Raw Methods - -Instead of using the `DB::raw` method, you may also use the following methods to insert a raw expression into various parts of your query. **Remember, Hypervel cannot guarantee that any query using raw expressions is protected against SQL injection vulnerabilities.** - -#### `selectRaw` - -The `selectRaw` method can be used in place of `addSelect(DB::raw(/* ... */))`. This method accepts an optional array of bindings as its second argument: - -```php -$orders = DB::table('orders') - ->selectRaw('price * ? as price_with_tax', [1.0825]) - ->get(); -``` - -#### `whereRaw / orWhereRaw` - -The `whereRaw` and `orWhereRaw` methods can be used to inject a raw "where" clause into your query. These methods accept an optional array of bindings as their second argument: - -```php -$orders = DB::table('orders') - ->whereRaw('price > IF(state = "TX", ?, 100)', [200]) - ->get(); -``` - -#### `havingRaw / orHavingRaw` - -The `havingRaw` and `orHavingRaw` methods may be used to provide a raw string as the value of the "having" clause. These methods accept an optional array of bindings as their second argument: - -```php -$orders = DB::table('orders') - ->select('department', DB::raw('SUM(price) as total_sales')) - ->groupBy('department') - ->havingRaw('SUM(price) > ?', [2500]) - ->get(); -``` - -#### `orderByRaw` - -The `orderByRaw` method may be used to provide a raw string as the value of the "order by" clause: - -```php -$orders = DB::table('orders') - ->orderByRaw('updated_at - created_at DESC') - ->get(); -``` - -### `groupByRaw` - -The `groupByRaw` method may be used to provide a raw string as the value of the `group by` clause: - -```php -$orders = DB::table('orders') - ->select('city', 'state') - ->groupByRaw('city, state') - ->get(); -``` - -## Joins - -#### Inner Join Clause - -The query builder may also be used to add join clauses to your queries. To perform a basic "inner join", you may use the `join` method on a query builder instance. The first argument passed to the `join` method is the name of the table you need to join to, while the remaining arguments specify the column constraints for the join. You may even join multiple tables in a single query: - -```php -use Hypervel\Support\Facades\DB; - -$users = DB::table('users') - ->join('contacts', 'users.id', '=', 'contacts.user_id') - ->join('orders', 'users.id', '=', 'orders.user_id') - ->select('users.*', 'contacts.phone', 'orders.price') - ->get(); -``` - -#### Left Join / Right Join Clause - -If you would like to perform a "left join" or "right join" instead of an "inner join", use the `leftJoin` or `rightJoin` methods. These methods have the same signature as the `join` method: - -```php -$users = DB::table('users') - ->leftJoin('posts', 'users.id', '=', 'posts.user_id') - ->get(); - -$users = DB::table('users') - ->rightJoin('posts', 'users.id', '=', 'posts.user_id') - ->get(); -``` - -#### Cross Join Clause - -You may use the `crossJoin` method to perform a "cross join". Cross joins generate a cartesian product between the first table and the joined table: - -```php -$sizes = DB::table('sizes') - ->crossJoin('colors') - ->get(); -``` - -#### Advanced Join Clauses - -You may also specify more advanced join clauses. To get started, pass a closure as the second argument to the `join` method. The closure will receive a `Hyperf\Database\Query\JoinClause` instance which allows you to specify constraints on the "join" clause: - -```php -DB::table('users') - ->join('contacts', function (JoinClause $join) { - $join->on('users.id', '=', 'contacts.user_id')->orOn(/* ... */); - }) - ->get(); -``` - -If you would like to use a "where" clause on your joins, you may use the `where` and `orWhere` methods provided by the `JoinClause` instance. Instead of comparing two columns, these methods will compare the column against a value: - -```php -DB::table('users') - ->join('contacts', function (JoinClause $join) { - $join->on('users.id', '=', 'contacts.user_id') - ->where('contacts.user_id', '>', 5); - }) - ->get(); -``` - -#### Subquery Joins - -You may use the `joinSub`, `leftJoinSub`, and `rightJoinSub` methods to join a query to a subquery. Each of these methods receives three arguments: the subquery, its table alias, and a closure that defines the related columns. In this example, we will retrieve a collection of users where each user record also contains the `created_at` timestamp of the user's most recently published blog post: - -```php -$latestPosts = DB::table('posts') - ->select('user_id', DB::raw('MAX(created_at) as last_post_created_at')) - ->where('is_published', true) - ->groupBy('user_id'); - -$users = DB::table('users') - ->joinSub($latestPosts, 'latest_posts', function (JoinClause $join) { - $join->on('users.id', '=', 'latest_posts.user_id'); - })->get(); -``` - -#### Lateral Joins - -::: warning -Lateral joins are currently supported by PostgreSQL, MySQL >= 8.0.14, and SQL Server. -::: - -You may use the `joinLateral` and `leftJoinLateral` methods to perform a "lateral join" with a subquery. Each of these methods receives two arguments: the subquery and its table alias. The join condition(s) should be specified within the `where` clause of the given subquery. Lateral joins are evaluated for each row and can reference columns outside the subquery. - -In this example, we will retrieve a collection of users as well as the user's three most recent blog posts. Each user can produce up to three rows in the result set: one for each of their most recent blog posts. The join condition is specified with a `whereColumn` clause within the subquery, referencing the current user row: - -```php -$latestPosts = DB::table('posts') - ->select('id as post_id', 'title as post_title', 'created_at as post_created_at') - ->whereColumn('user_id', 'users.id') - ->orderBy('created_at', 'desc') - ->limit(3); - -$users = DB::table('users') - ->joinLateral($latestPosts, 'latest_posts') - ->get(); -``` - -## Unions - -The query builder also provides a convenient method to "union" two or more queries together. For example, you may create an initial query and use the `union` method to union it with more queries: - -```php -use Hypervel\Support\Facades\DB; - -$first = DB::table('users') - ->whereNull('first_name'); - -$users = DB::table('users') - ->whereNull('last_name') - ->union($first) - ->get(); -``` - -In addition to the `union` method, the query builder provides a `unionAll` method. Queries that are combined using the `unionAll` method will not have their duplicate results removed. The `unionAll` method has the same method signature as the `union` method. - -## Basic Where Clauses - -### Where Clauses - -You may use the query builder's `where` method to add "where" clauses to the query. The most basic call to the `where` method requires three arguments. The first argument is the name of the column. The second argument is an operator, which can be any of the database's supported operators. The third argument is the value to compare against the column's value. - -For example, the following query retrieves users where the value of the `votes` column is equal to `100` and the value of the `age` column is greater than `35`: - -```php -$users = DB::table('users') - ->where('votes', '=', 100) - ->where('age', '>', 35) - ->get(); -``` - -For convenience, if you want to verify that a column is `=` to a given value, you may pass the value as the second argument to the `where` method. Hypervel will assume you would like to use the `=` operator: - -```php -$users = DB::table('users')->where('votes', 100)->get(); -``` - -As previously mentioned, you may use any operator that is supported by your database system: - -```php -$users = DB::table('users') - ->where('votes', '>=', 100) - ->get(); - -$users = DB::table('users') - ->where('votes', '<>', 100) - ->get(); - -$users = DB::table('users') - ->where('name', 'like', 'T%') - ->get(); -``` - -You may also pass an array of conditions to the `where` function. Each element of the array should be an array containing the three arguments typically passed to the `where` method: - -```php -$users = DB::table('users')->where([ - ['status', '=', '1'], - ['subscribed', '<>', '1'], -])->get(); -``` - -::: warning -PDO does not support binding column names. Therefore, you should never allow user input to dictate the column names referenced by your queries, including "order by" columns. -::: - -::: warning -MySQL and MariaDB automatically typecast strings to integers in string-number comparisons. In this process, non-numeric strings are converted to `0`, which can lead to unexpected results. For example, if your table has a `secret` column with a value of `aaa` and you run `User::where('secret', 0)`, that row will be returned. To avoid this, ensure all values are typecast to their appropriate types before using them in queries. -::: - -### Or Where Clauses - -When chaining together calls to the query builder's `where` method, the "where" clauses will be joined together using the `and` operator. However, you may use the `orWhere` method to join a clause to the query using the `or` operator. The `orWhere` method accepts the same arguments as the `where` method: - -```php -$users = DB::table('users') - ->where('votes', '>', 100) - ->orWhere('name', 'John') - ->get(); -``` - -If you need to group an "or" condition within parentheses, you may pass a closure as the first argument to the `orWhere` method: - -```php -$users = DB::table('users') - ->where('votes', '>', 100) - ->orWhere(function (Builder $query) { - $query->where('name', 'Abigail') - ->where('votes', '>', 50); - }) - ->get(); -``` - -The example above will produce the following SQL: - -```sql -select * from users where votes > 100 or (name = 'Abigail' and votes > 50) -``` - -::: warning -You should always group `orWhere` calls in order to avoid unexpected behavior when global scopes are applied. -::: - -### Where Not Clauses - -The `whereNot` and `orWhereNot` methods may be used to negate a given group of query constraints. For example, the following query excludes products that are on clearance or which have a price that is less than ten: - -```php -$products = DB::table('products') - ->whereNot(function (Builder $query) { - $query->where('clearance', true) - ->orWhere('price', '<', 10); - }) - ->get(); -``` - -### Where Any / All / None Clauses - -Sometimes you may need to apply the same query constraints to multiple columns. For example, you may want to retrieve all records where any columns in a given list are `LIKE` a given value. You may accomplish this using the `whereAny` method: - -```php -$users = DB::table('users') - ->where('active', true) - ->whereAny([ - 'name', - 'email', - 'phone', - ], 'like', 'Example%') - ->get(); -``` - -The query above will result in the following SQL: - -```sql -SELECT * -FROM users -WHERE active = true AND ( - name LIKE 'Example%' OR - email LIKE 'Example%' OR - phone LIKE 'Example%' -) -``` - -Similarly, the `whereAll` method may be used to retrieve records where all of the given columns match a given constraint: - -```php -$posts = DB::table('posts') - ->where('published', true) - ->whereAll([ - 'title', - 'content', - ], 'like', '%Laravel%') - ->get(); -``` - -The query above will result in the following SQL: - -```sql -SELECT * -FROM posts -WHERE published = true AND ( - title LIKE '%Laravel%' AND - content LIKE '%Laravel%' -) -``` - -The `whereNone` method may be used to retrieve records where none of the given columns match a given constraint: - -```php -$posts = DB::table('albums') - ->where('published', true) - ->whereNone([ - 'title', - 'lyrics', - 'tags', - ], 'like', '%explicit%') - ->get(); -``` - -The query above will result in the following SQL: - -```sql -SELECT * -FROM albums -WHERE published = true AND NOT ( - title LIKE '%explicit%' OR - lyrics LIKE '%explicit%' OR - tags LIKE '%explicit%' -) -``` - -### JSON Where Clauses - -Hypervel also supports querying JSON column types on databases that provide support for JSON column types. Currently, this includes MariaDB 10.3+, MySQL 8.0+, PostgreSQL 12.0+, SQL Server 2017+, and SQLite 3.39.0+. To query a JSON column, use the `->` operator: - -```php -$users = DB::table('users') - ->where('preferences->dining->meal', 'salad') - ->get(); -``` - -You may use `whereJsonContains` to query JSON arrays: - -```php -$users = DB::table('users') - ->whereJsonContains('options->languages', 'en') - ->get(); -``` - -If your application uses the MariaDB, MySQL, or PostgreSQL databases, you may pass an array of values to the `whereJsonContains` method: - -```php -$users = DB::table('users') - ->whereJsonContains('options->languages', ['en', 'de']) - ->get(); -``` - -You may use `whereJsonLength` method to query JSON arrays by their length: - -```php -$users = DB::table('users') - ->whereJsonLength('options->languages', 0) - ->get(); - -$users = DB::table('users') - ->whereJsonLength('options->languages', '>', 1) - ->get(); -``` - -### Additional Where Clauses - -**whereIn / whereNotIn / orWhereIn / orWhereNotIn** - -The `whereIn` method verifies that a given column's value is contained within the given array: - -```php -$users = DB::table('users') - ->whereIn('id', [1, 2, 3]) - ->get(); -``` - -The `whereNotIn` method verifies that the given column's value is not contained in the given array: - -```php -$users = DB::table('users') - ->whereNotIn('id', [1, 2, 3]) - ->get(); -``` - -You may also provide a query object as the `whereIn` method's second argument: - -```php -$activeUsers = DB::table('users')->select('id')->where('is_active', 1); - -$users = DB::table('comments') - ->whereIn('user_id', $activeUsers) - ->get(); -``` - -The example above will produce the following SQL: - -```sql -select * from comments where user_id in ( - select id - from users - where is_active = 1 -) -``` - -::: warning -If you are adding a large array of integer bindings to your query, the `whereIntegerInRaw` or `whereIntegerNotInRaw` methods may be used to greatly reduce your memory usage. -::: - -**whereBetween / orWhereBetween** - -The `whereBetween` method verifies that a column's value is between two values: - -```php -$users = DB::table('users') - ->whereBetween('votes', [1, 100]) - ->get(); -``` - -**whereNotBetween / orWhereNotBetween** - -The `whereNotBetween` method verifies that a column's value lies outside of two values: - -```php -$users = DB::table('users') - ->whereNotBetween('votes', [1, 100]) - ->get(); -``` - -**whereNull / whereNotNull / orWhereNull / orWhereNotNull** - -The `whereNull` method verifies that the value of the given column is `NULL`: - -```php -$users = DB::table('users') - ->whereNull('updated_at') - ->get(); -``` - -The `whereNotNull` method verifies that the column's value is not `NULL`: - -```php -$users = DB::table('users') - ->whereNotNull('updated_at') - ->get(); -``` - -**whereDate / whereMonth / whereDay / whereYear / whereTime** - -The `whereDate` method may be used to compare a column's value against a date: - -```php -$users = DB::table('users') - ->whereDate('created_at', '2016-12-31') - ->get(); -``` - -The `whereMonth` method may be used to compare a column's value against a specific month: - -```php -$users = DB::table('users') - ->whereMonth('created_at', '12') - ->get(); -``` - -The `whereDay` method may be used to compare a column's value against a specific day of the month: - -```php -$users = DB::table('users') - ->whereDay('created_at', '31') - ->get(); -``` - -The `whereYear` method may be used to compare a column's value against a specific year: - -```php -$users = DB::table('users') - ->whereYear('created_at', '2016') - ->get(); -``` - -The `whereTime` method may be used to compare a column's value against a specific time: - -```php -$users = DB::table('users') - ->whereTime('created_at', '=', '11:20:45') - ->get(); -``` - -**whereColumn / orWhereColumn** - -The `whereColumn` method may be used to verify that two columns are equal: - -```php -$users = DB::table('users') - ->whereColumn('first_name', 'last_name') - ->get(); -``` - -You may also pass a comparison operator to the `whereColumn` method: - -```php -$users = DB::table('users') - ->whereColumn('updated_at', '>', 'created_at') - ->get(); -``` - -You may also pass an array of column comparisons to the `whereColumn` method. These conditions will be joined using the `and` operator: - -```php -$users = DB::table('users') - ->whereColumn([ - ['first_name', '=', 'last_name'], - ['updated_at', '>', 'created_at'], - ])->get(); -``` - -### Logical Grouping - -Sometimes you may need to group several "where" clauses within parentheses in order to achieve your query's desired logical grouping. In fact, you should generally always group calls to the `orWhere` method in parentheses in order to avoid unexpected query behavior. To accomplish this, you may pass a closure to the `where` method: - -```php -$users = DB::table('users') - ->where('name', '=', 'John') - ->where(function (Builder $query) { - $query->where('votes', '>', 100) - ->orWhere('title', '=', 'Admin'); - }) - ->get(); -``` - -As you can see, passing a closure into the `where` method instructs the query builder to begin a constraint group. The closure will receive a query builder instance which you can use to set the constraints that should be contained within the parenthesis group. The example above will produce the following SQL: - -```sql -select * from users where name = 'John' and (votes > 100 or title = 'Admin') -``` - -::: warning -You should always group `orWhere` calls in order to avoid unexpected behavior when global scopes are applied. -::: - -## Advanced Where Clauses - -### Where Exists Clauses - -The `whereExists` method allows you to write "where exists" SQL clauses. The `whereExists` method accepts a closure which will receive a query builder instance, allowing you to define the query that should be placed inside of the "exists" clause: - -```php -$users = DB::table('users') - ->whereExists(function (Builder $query) { - $query->select(DB::raw(1)) - ->from('orders') - ->whereColumn('orders.user_id', 'users.id'); - }) - ->get(); -``` - -Alternatively, you may provide a query object to the `whereExists` method instead of a closure: - -```php -$orders = DB::table('orders') - ->select(DB::raw(1)) - ->whereColumn('orders.user_id', 'users.id'); - -$users = DB::table('users') - ->whereExists($orders) - ->get(); -``` - -Both of the examples above will produce the following SQL: - -```sql -select * from users -where exists ( - select 1 - from orders - where orders.user_id = users.id -) -``` - -### Subquery Where Clauses - -Sometimes you may need to construct a "where" clause that compares the results of a subquery to a given value. You may accomplish this by passing a closure and a value to the `where` method. For example, the following query will retrieve all users who have a recent "membership" of a given type; - -```php -use App\Models\User; -use Hyperf\Database\Query\Builder; - -$users = User::where(function (Builder $query) { - $query->select('type') - ->from('membership') - ->whereColumn('membership.user_id', 'users.id') - ->orderByDesc('membership.start_date') - ->limit(1); -}, 'Pro')->get(); -``` - -Or, you may need to construct a "where" clause that compares a column to the results of a subquery. You may accomplish this by passing a column, operator, and closure to the `where` method. For example, the following query will retrieve all income records where the amount is less than average; - -```php -use App\Models\Income; -use Hyperf\Database\Query\Builder; - -$incomes = Income::where('amount', '<', function (Builder $query) { - $query->selectRaw('avg(i.amount)')->from('incomes as i'); -})->get(); -``` - -### Full Text Where Clauses - -::: warning -Full text where clauses are currently supported by MariaDB, MySQL, and PostgreSQL. -::: - -The `whereFullText` and `orWhereFullText` methods may be used to add full text "where" clauses to a query for columns that have [full text indexes](/docs/migrations#available-index-types). These methods will be transformed into the appropriate SQL for the underlying database system by Laravel. For example, a `MATCH AGAINST` clause will be generated for applications utilizing MariaDB or MySQL: - -```php -$users = DB::table('users') - ->whereFullText('bio', 'web developer') - ->get(); -``` - -## Ordering, Grouping, Limit and Offset - -### Ordering - -#### The `orderBy` Method - -The `orderBy` method allows you to sort the results of the query by a given column. The first argument accepted by the `orderBy` method should be the column you wish to sort by, while the second argument determines the direction of the sort and may be either `asc` or `desc`: - -```php -$users = DB::table('users') - ->orderBy('name', 'desc') - ->get(); -``` - -To sort by multiple columns, you may simply invoke `orderBy` as many times as necessary: - -```php -$users = DB::table('users') - ->orderBy('name', 'desc') - ->orderBy('email', 'asc') - ->get(); -``` - -#### The `latest` and `oldest` Methods - -The `latest` and `oldest` methods allow you to easily order results by date. By default, the result will be ordered by the table's `created_at` column. Or, you may pass the column name that you wish to sort by: - -```php -$user = DB::table('users') - ->latest() - ->first(); -``` - -#### Random Ordering - -The `inRandomOrder` method may be used to sort the query results randomly. For example, you may use this method to fetch a random user: - -```php -$randomUser = DB::table('users') - ->inRandomOrder() - ->first(); -``` - -#### Removing Existing Orderings - -The `reorder` method removes all of the "order by" clauses that have previously been applied to the query: - -```php -$query = DB::table('users')->orderBy('name'); - -$unorderedUsers = $query->reorder()->get(); -``` - -You may pass a column and direction when calling the `reorder` method in order to remove all existing "order by" clauses and apply an entirely new order to the query: - -```php -$query = DB::table('users')->orderBy('name'); - -$usersOrderedByEmail = $query->reorder('email', 'desc')->get(); -``` - -### Grouping - -#### The `groupBy` and `having` Methods - -As you might expect, the `groupBy` and `having` methods may be used to group the query results. The `having` method's signature is similar to that of the `where` method: - -```php -$users = DB::table('users') - ->groupBy('account_id') - ->having('account_id', '>', 100) - ->get(); -``` - -You can use the `havingBetween` method to filter the results within a given range: - -```php -$report = DB::table('orders') - ->selectRaw('count(id) as number_of_orders, customer_id') - ->groupBy('customer_id') - ->havingBetween('number_of_orders', [5, 15]) - ->get(); -``` - -You may pass multiple arguments to the `groupBy` method to group by multiple columns: - -```php -$users = DB::table('users') - ->groupBy('first_name', 'status') - ->having('account_id', '>', 100) - ->get(); -``` - -To build more advanced `having` statements, see the [`havingRaw`](#raw-methods) method. - -### Limit and Offset - -#### The `skip` and `take` Methods - -You may use the `skip` and `take` methods to limit the number of results returned from the query or to skip a given number of results in the query: - -```php -$users = DB::table('users')->skip(10)->take(5)->get(); -``` - -Alternatively, you may use the `limit` and `offset` methods. These methods are functionally equivalent to the `take` and `skip` methods, respectively: - -```php -$users = DB::table('users') - ->offset(10) - ->limit(5) - ->get(); -``` - -## Conditional Clauses - -Sometimes you may want certain query clauses to apply to a query based on another condition. For instance, you may only want to apply a `where` statement if a given input value is present on the incoming HTTP request. You may accomplish this using the `when` method: - -```php -$role = $request->input('role'); - -$users = DB::table('users') - ->when($role, function (Builder $query, string $role) { - $query->where('role_id', $role); - }) - ->get(); -``` - -The `when` method only executes the given closure when the first argument is `true`. If the first argument is `false`, the closure will not be executed. So, in the example above, the closure given to the `when` method will only be invoked if the `role` field is present on the incoming request and evaluates to `true`. - -You may pass another closure as the third argument to the `when` method. This closure will only execute if the first argument evaluates as `false`. To illustrate how this feature may be used, we will use it to configure the default ordering of a query: - -```php -$sortByVotes = $request->boolean('sort_by_votes'); - -$users = DB::table('users') - ->when($sortByVotes, function (Builder $query, bool $sortByVotes) { - $query->orderBy('votes'); - }, function (Builder $query) { - $query->orderBy('name'); - }) - ->get(); -``` - -## Insert Statements - -The query builder also provides an `insert` method that may be used to insert records into the database table. The `insert` method accepts an array of column names and values: - -```php -DB::table('users')->insert([ - 'email' => 'kayla@example.com', - 'votes' => 0 -]); -``` - -You may insert several records at once by passing an array of arrays. Each array represents a record that should be inserted into the table: - -```php -DB::table('users')->insert([ - ['email' => 'picard@example.com', 'votes' => 0], - ['email' => 'janeway@example.com', 'votes' => 0], -]); -``` - -The `insertOrIgnore` method will ignore errors while inserting records into the database. When using this method, you should be aware that duplicate record errors will be ignored and other types of errors may also be ignored depending on the database engine. For example, `insertOrIgnore` will [bypass MySQL's strict mode](https://dev.mysql.com/doc/refman/en/sql-mode.html#ignore-effect-on-execution): - -```php -DB::table('users')->insertOrIgnore([ - ['id' => 1, 'email' => 'sisko@example.com'], - ['id' => 2, 'email' => 'archer@example.com'], -]); -``` - -The `insertUsing` method will insert new records into the table while using a subquery to determine the data that should be inserted: - -```php -DB::table('pruned_users')->insertUsing([ - 'id', 'name', 'email', 'email_verified_at' -], DB::table('users')->select( - 'id', 'name', 'email', 'email_verified_at' -)->where('updated_at', '<=', now()->subMonth())); -``` - -#### Auto-Incrementing IDs - -If the table has an auto-incrementing id, use the `insertGetId` method to insert a record and then retrieve the ID: - -```php -$id = DB::table('users')->insertGetId( - ['email' => 'john@example.com', 'votes' => 0] -); -``` - -::: warning -When using PostgreSQL the `insertGetId` method expects the auto-incrementing column to be named `id`. If you would like to retrieve the ID from a different "sequence", you may pass the column name as the second parameter to the `insertGetId` method. -::: - -### Upserts - -The `upsert` method will insert records that do not exist and update the records that already exist with new values that you may specify. The method's first argument consists of the values to insert or update, while the second argument lists the column(s) that uniquely identify records within the associated table. The method's third and final argument is an array of columns that should be updated if a matching record already exists in the database: - -```php -DB::table('flights')->upsert( - [ - ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99], - ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150] - ], - ['departure', 'destination'], - ['price'] -); -``` - -In the example above, Hypervel will attempt to insert two records. If a record already exists with the same `departure` and `destination` column values, Hypervel will update that record's `price` column. - -::: warning -All databases except SQL Server require the columns in the second argument of the `upsert` method to have a "primary" or "unique" index. In addition, the MariaDB and MySQL database drivers ignore the second argument of the `upsert` method and always use the "primary" and "unique" indexes of the table to detect existing records. -::: - -## Update Statements - -In addition to inserting records into the database, the query builder can also update existing records using the `update` method. The `update` method, like the `insert` method, accepts an array of column and value pairs indicating the columns to be updated. The `update` method returns the number of affected rows. You may constrain the `update` query using `where` clauses: - -```php -$affected = DB::table('users') - ->where('id', 1) - ->update(['votes' => 1]); -``` - -#### Update or Insert - -Sometimes you may want to update an existing record in the database or create it if no matching record exists. In this scenario, the `updateOrInsert` method may be used. The `updateOrInsert` method accepts two arguments: an array of conditions by which to find the record, and an array of column and value pairs indicating the columns to be updated. - -The `updateOrInsert` method will attempt to locate a matching database record using the first argument's column and value pairs. If the record exists, it will be updated with the values in the second argument. If the record cannot be found, a new record will be inserted with the merged attributes of both arguments: - -```php -DB::table('users') - ->updateOrInsert( - ['email' => 'john@example.com', 'name' => 'John'], - ['votes' => '2'] - ); -``` - -You may provide a closure to the `updateOrInsert` method to customize the attributes that are updated or inserted into the database based on the existence of a matching record: - -```php -DB::table('users')->updateOrInsert( - ['user_id' => $user_id], - fn ($exists) => $exists ? [ - 'name' => $data['name'], - 'email' => $data['email'], - ] : [ - 'name' => $data['name'], - 'email' => $data['email'], - 'marketable' => true, - ], -); -``` - -### Updating JSON Columns - -When updating a JSON column, you should use `->` syntax to update the appropriate key in the JSON object. This operation is supported on MariaDB 10.3+, MySQL 5.7+, and PostgreSQL 9.5+: - -```php -$affected = DB::table('users') - ->where('id', 1) - ->update(['options->enabled' => true]); -``` - -### Increment and Decrement - -The query builder also provides convenient methods for incrementing or decrementing the value of a given column. Both of these methods accept at least one argument: the column to modify. A second argument may be provided to specify the amount by which the column should be incremented or decremented: - -```php -DB::table('users')->increment('votes'); - -DB::table('users')->increment('votes', 5); - -DB::table('users')->decrement('votes'); - -DB::table('users')->decrement('votes', 5); -``` - -If needed, you may also specify additional columns to update during the increment or decrement operation: - -```php -DB::table('users')->increment('votes', 1, ['name' => 'John']); -``` - -In addition, you may increment or decrement multiple columns at once using the `incrementEach` and `decrementEach` methods: - -```php -DB::table('users')->incrementEach([ - 'votes' => 5, - 'balance' => 100, -]); -``` - -## Delete Statements - -The query builder's `delete` method may be used to delete records from the table. The `delete` method returns the number of affected rows. You may constrain `delete` statements by adding "where" clauses before calling the `delete` method: - -```php -$deleted = DB::table('users')->delete(); - -$deleted = DB::table('users')->where('votes', '>', 100)->delete(); -``` - -## Pessimistic Locking - -The query builder also includes a few functions to help you achieve "pessimistic locking" when executing your `select` statements. To execute a statement with a "shared lock", you may call the `sharedLock` method. A shared lock prevents the selected rows from being modified until your transaction is committed: - -```php -DB::table('users') - ->where('votes', '>', 100) - ->sharedLock() - ->get(); -``` - -Alternatively, you may use the `lockForUpdate` method. A "for update" lock prevents the selected records from being modified or from being selected with another shared lock: - -```php -DB::table('users') - ->where('votes', '>', 100) - ->lockForUpdate() - ->get(); -``` - -While not obligatory, it is recommended to wrap pessimistic locks within a [transaction](/docs/database#database-transactions). This ensures that the data retrieved remains unaltered in the database until the entire operation completes. In case of a failure, the transaction will roll back any changes and release the locks automatically: - -```php -DB::transaction(function () { - $sender = DB::table('users') - ->lockForUpdate() - ->find(1); - - $receiver = DB::table('users') - ->lockForUpdate(); - ->find(2); - - if ($sender->balance < 100) { - throw new RuntimeException('Balance too low.'); - } - - DB::table('users') - ->where('id', $sender->id) - ->update([ - 'balance' => $sender->balance - 100 - ]); - - DB::table('users') - ->where('id', $receiver->id) - ->update([ - 'balance' => $receiver->balance + 100 - ]); -}); -``` - -## Debugging - -You may use the `toSql` and `toRawSql` methods while building a query to dump the current query bindings and SQL. The `toSql` method will display the compiled sql `without` binding parameters. The `toRawSql` method will display the compiled sql `with` binding parameters: - -```php -DB::table('users')->where('votes', '>', 100)->toSql(); - -DB::table('users')->where('votes', '>', 100)->toRawSql(); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/queues.md b/_archive/docs/0.3/docs/queues.md deleted file mode 100644 index a7b133da70..0000000000 --- a/_archive/docs/0.3/docs/queues.md +++ /dev/null @@ -1,2334 +0,0 @@ -# Queues -[[toc]] - -## Introduction - -While building your web application, you may have some tasks, such as parsing and storing an uploaded CSV file, that take too long to perform during a typical web request. Thankfully, Hypervel allows you to easily create queued jobs that may be processed in the background. By moving time intensive tasks to a queue, your application can respond to web requests with blazing speed and provide a better user experience to your customers. - -Hypervel queues provide a unified queueing API across a variety of different queue backends, such as [Amazon SQS](https://aws.amazon.com/sqs/), [Redis](https://redis.io), or even a relational database. - -Hypervel's queue configuration options are stored in your application's `config/queue.php` configuration file. In this file, you will find connection configurations for each of the queue drivers that are included with the framework, including the database, [Amazon SQS](https://aws.amazon.com/sqs/), [Redis](https://redis.io), and [Beanstalkd](https://beanstalkd.github.io/) drivers, as well as a synchronous driver that will execute jobs immediately (for use during local development). A `null` queue driver is also included which discards queued jobs. - -For those jobs you just want to make them executed asynchronously without promising storage persistency, you can use `coroutine` or `defer` driver to run these jobs before the end of current coroutine's lifecycle. Please note defer itself is FILO, which is opposite to queue structure. - -::: note -Hypervel hasn't support horizon yet. -::: - -### Connections vs. Queues - -Before getting started with Hypervel queues, it is important to understand the distinction between "connections" and "queues". In your `config/queue.php` configuration file, there is a `connections` configuration array. This option defines the connections to backend queue services such as Amazon SQS, Beanstalk, or Redis. However, any given queue connection may have multiple "queues" which may be thought of as different stacks or piles of queued jobs. - -Note that each connection configuration example in the `queue` configuration file contains a `queue` attribute. This is the default queue that jobs will be dispatched to when they are sent to a given connection. In other words, if you dispatch a job without explicitly defining which queue it should be dispatched to, the job will be placed on the queue that is defined in the `queue` attribute of the connection configuration: - -```php -use App\Jobs\ProcessPodcast; - -// This job is sent to the default connection's default queue... -ProcessPodcast::dispatch(); - -// This job is sent to the default connection's "emails" queue... -ProcessPodcast::dispatch()->onQueue('emails'); -``` - -Some applications may not need to ever push jobs onto multiple queues, instead preferring to have one simple queue. However, pushing jobs to multiple queues can be especially useful for applications that wish to prioritize or segment how jobs are processed, since the Hypervel queue worker allows you to specify which queues it should process by priority. For example, if you push jobs to a `high` queue, you may run a worker that gives them higher processing priority: - -```shell:no-line-numbers -php artisan queue:work --queue=high,default -``` - -### Driver Notes and Prerequisites - -#### Database - -In order to use the `database` queue driver, you will need a database table to hold the jobs. To generate a migration that creates this table, run the `queue:table` Artisan command. Once the migration has been created, you may migrate your database using the `migrate` command: - -```shell:no-line-numbers -php artisan queue:table - -php artisan migrate -``` - -Finally, don't forget to instruct your application to use the `database` driver by updating the `QUEUE_CONNECTION` variable in your application's `.env` file: - -``` -QUEUE_CONNECTION=database -``` - -#### Redis - -In order to use the `redis` queue driver, you should configure a Redis database connection in your `config/database.php` configuration file. - -::: warning -The `serializer` and `compression` Redis options are not supported by the `redis` queue driver. -::: - -**Redis Cluster** - -If your Redis queue connection uses a Redis Cluster, your queue names must contain a [key hash tag](https://redis.io/docs/reference/cluster-spec/#hash-tags). This is required in order to ensure all of the Redis keys for a given queue are placed into the same hash slot: - -```php -'redis' => [ - 'driver' => 'redis', - 'connection' => 'default', - 'queue' => '{default}', - 'retry_after' => 90, -], -``` - -**Blocking** - -When using the Redis queue, you may use the `block_for` configuration option to specify how long the driver should wait for a job to become available before iterating through the worker loop and re-polling the Redis database. - -Adjusting this value based on your queue load can be more efficient than continually polling the Redis database for new jobs. For instance, you may set the value to `5` to indicate that the driver should block for five seconds while waiting for a job to become available: - -```php -'redis' => [ - 'driver' => 'redis', - 'connection' => 'default', - 'queue' => 'default', - 'retry_after' => 90, - 'block_for' => 5, -], -``` - -::: warning -Setting `block_for` to `0` will cause queue workers to block indefinitely until a job is available. This will also prevent signals such as `SIGTERM` from being handled until the next job has been processed. -::: - -#### Other Driver Prerequisites - -The following dependencies are needed for the listed queue drivers. These dependencies may be installed via the Composer package manager: - -
      - -- Amazon SQS: `aws/aws-sdk-php ~3.0` -- Beanstalkd: `pda/pheanstalk ~4.0` -- Redis: `predis/predis ~1.0` or phpredis PHP extension - -
      - -## Creating Jobs - -### Generating Job Classes - -By default, all of the queueable jobs for your application are stored in the `app/Jobs` directory. If the `app/Jobs` directory doesn't exist, it will be created when you run the `make:job` Artisan command: - -```shell:no-line-numbers -php artisan make:job ProcessPodcast -``` - -The generated class will implement the `Hypervel\Queue\Contracts\ShouldQueue` interface, indicating to Hypervel that the job should be pushed onto the queue to run asynchronously. - -### Class Structure - -Job classes are very simple, normally containing only a `handle` method that is invoked when the job is processed by the queue. To get started, let's take a look at an example job class. In this example, we'll pretend we manage a podcast publishing service and need to process the uploaded podcast files before they are published: - -```php -app->bindMethod([ProcessPodcast::class, 'handle'], function (ProcessPodcast $job, Application $app) { - return $job->handle($app->make(AudioProcessor::class)); -}); -``` - -::: warning -Binary data, such as raw image contents, should be passed through the `base64_encode` function before being passed to a queued job. Otherwise, the job may not properly serialize to JSON when being placed on the queue. -::: - -#### Queued Relationships - -Because all loaded Eloquent model relationships also get serialized when a job is queued, the serialized job string can sometimes become quite large. Furthermore, when a job is deserialized and model relationships are re-retrieved from the database, they will be retrieved in their entirety. Any previous relationship constraints that were applied before the model was serialized during the job queueing process will not be applied when the job is deserialized. Therefore, if you wish to work with a subset of a given relationship, you should re-constrain that relationship within your queued job. - -Or, to prevent relations from being serialized, you can call the `withoutRelations` method on the model when setting a property value. This method will return an instance of the model without its loaded relationships: - -```php -/** - * Create a new job instance. - */ -public function __construct(Podcast $podcast) -{ - $this->podcast = $podcast->withoutRelations(); -} -``` - -If a job receives a collection or array of Eloquent models instead of a single model, the models within that collection will not have their relationships restored when the job is deserialized and executed. This is to prevent excessive resource usage on jobs that deal with large numbers of models. - -### Unique Jobs - -::: warning -Unique jobs require a cache driver that supports [locks](/docs/cache#atomic-locks). Currently, the `memcached`, `redis`, `dynamodb`, `database`, `file`, and `array` cache drivers support atomic locks. In addition, unique job constraints do not apply to jobs within batches. -::: - -Sometimes, you may want to ensure that only one instance of a specific job is on the queue at any point in time. You may do so by implementing the `ShouldBeUnique` interface on your job class. This interface does not require you to define any additional methods on your class: - -```php -product->id; - } -} -``` - -In the example above, the `UpdateSearchIndex` job is unique by a product ID. So, any new dispatches of the job with the same product ID will be ignored until the existing job has completed processing. In addition, if the existing job is not processed within one hour, the unique lock will be released and another job with the same unique key can be dispatched to the queue. - -::: warning -If your application dispatches jobs from multiple web servers or containers, you should ensure that all of your servers are communicating with the same central cache server so that Hypervel can accurately determine if a job is unique. -::: - -#### Keeping Jobs Unique Until Processing Begins - -By default, unique jobs are "unlocked" after a job completes processing or fails all of its retry attempts. However, there may be situations where you would like your job to unlock immediately before it is processed. To accomplish this, your job should implement the `ShouldBeUniqueUntilProcessing` contract instead of the `ShouldBeUnique` contract: - -```php -block(0)->allow(1)->every(5) - ->then(function () use ($job, $next) { - // Lock obtained... - - $next($job); - }, function () use ($job) { - // Could not obtain lock... - - $job->release(5); - }); - } -} -``` - -As you can see, like [route middleware](/docs/middleware), job middleware receive the job being processed and a callback that should be invoked to continue processing the job. - -After creating job middleware, they may be attached to a job by returning them from the job's `middleware` method. This method does not exist on jobs scaffolded by the `make:job` Artisan command, so you will need to manually add it to your job class: - -```php -use App\Jobs\Middleware\RateLimited; - -/** - * Get the middleware the job should pass through. - * - * @return array - */ -public function middleware(): array -{ - return [new RateLimited]; -} -``` - -::: note -Job middleware can also be assigned to queueable event listeners, mailables, and notifications. -::: - -### Rate Limiting - -Although we just demonstrated how to write your own rate limiting job middleware, Hypervel actually includes a rate limiting middleware that you may utilize to rate limit jobs. Like [route rate limiters](/docs/routing#defining-rate-limiters), job rate limiters are defined using the `RateLimiter` facade's `for` method. - -For example, you may wish to allow users to backup their data once per hour while imposing no such limit on premium customers. To accomplish this, you may define a `RateLimiter` in the `boot` method of your `AppServiceProvider`: - -```php -use Hypervel\Cache\RateLimiting\Limit; -use Hypervel\Support\Facades\RateLimiter; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - RateLimiter::for('backups', function (object $job) { - return $job->user->vipCustomer() - ? Limit::none() - : Limit::perHour(1)->by($job->user->id); - }); -} -``` - -In the example above, we defined an hourly rate limit; however, you may easily define a rate limit based on minutes using the `perMinute` method. In addition, you may pass any value you wish to the `by` method of the rate limit; however, this value is most often used to segment rate limits by customer: - -```php -return Limit::perMinute(50)->by($job->user->id); -``` - -Once you have defined your rate limit, you may attach the rate limiter to your job using the `Hypervel\Queue\Middleware\RateLimited` middleware. Each time the job exceeds the rate limit, this middleware will release the job back to the queue with an appropriate delay based on the rate limit duration. - -```php -use Hypervel\Queue\Middleware\RateLimited; - -/** - * Get the middleware the job should pass through. - * - * @return array - */ -public function middleware(): array -{ - return [new RateLimited('backups')]; -} -``` - -Releasing a rate limited job back onto the queue will still increment the job's total number of `attempts`. You may wish to tune your `tries` and `maxExceptions` properties on your job class accordingly. Or, you may wish to use the [`retryUntil` method](#time-based-attempts) to define the amount of time until the job should no longer be attempted. - -If you do not want a job to be retried when it is rate limited, you may use the `dontRelease` method: - -```php -/** - * Get the middleware the job should pass through. - * - * @return array - */ -public function middleware(): array -{ - return [(new RateLimited('backups'))->dontRelease()]; -} -``` - -::: note -If you are using Redis, you may use the `Hypervel\Queue\Middleware\RateLimitedWithRedis` middleware, which is fine-tuned for Redis and more efficient than the basic rate limiting middleware. -::: - -### Preventing Job Overlaps - -Hypervel includes an `Hypervel\Queue\Middleware\WithoutOverlapping` middleware that allows you to prevent job overlaps based on an arbitrary key. This can be helpful when a queued job is modifying a resource that should only be modified by one job at a time. - -For example, let's imagine you have a queued job that updates a user's credit score and you want to prevent credit score update job overlaps for the same user ID. To accomplish this, you can return the `WithoutOverlapping` middleware from your job's `middleware` method: - -```php -use Hypervel\Queue\Middleware\WithoutOverlapping; - -/** - * Get the middleware the job should pass through. - * - * @return array - */ -public function middleware(): array -{ - return [new WithoutOverlapping($this->user->id)]; -} -``` - -Any overlapping jobs of the same type will be released back to the queue. You may also specify the number of seconds that must elapse before the released job will be attempted again: - -```php -/** - * Get the middleware the job should pass through. - * - * @return array - */ -public function middleware(): array -{ - return [(new WithoutOverlapping($this->order->id))->releaseAfter(60)]; -} -``` - -If you wish to immediately delete any overlapping jobs so that they will not be retried, you may use the `dontRelease` method: - -```php -/** - * Get the middleware the job should pass through. - * - * @return array - */ -public function middleware(): array -{ - return [(new WithoutOverlapping($this->order->id))->dontRelease()]; -} -``` - -The `WithoutOverlapping` middleware is powered by Hypervel's atomic lock feature. Sometimes, your job may unexpectedly fail or timeout in such a way that the lock is not released. Therefore, you may explicitly define a lock expiration time using the `expireAfter` method. For example, the example below will instruct Hypervel to release the `WithoutOverlapping` lock three minutes after the job has started processing: - -```php -/** - * Get the middleware the job should pass through. - * - * @return array - */ -public function middleware(): array -{ - return [(new WithoutOverlapping($this->order->id))->expireAfter(180)]; -} -``` - -::: warning -> The `WithoutOverlapping` middleware requires a cache driver that supports [locks](/docs/cache#atomic-locks). Currently, the `memcached`, `redis`, `dynamodb`, `database`, `file`, and `array` cache drivers support atomic locks. -::: - -#### Sharing Lock Keys Across Job Classes - -By default, the `WithoutOverlapping` middleware will only prevent overlapping jobs of the same class. So, although two different job classes may use the same lock key, they will not be prevented from overlapping. However, you can instruct Hypervel to apply the key across job classes using the `shared` method: - -```php -use Hypervel\Queue\Middleware\WithoutOverlapping; - -class ProviderIsDown -{ - // ... - - - public function middleware(): array - { - return [ - (new WithoutOverlapping("status:{$this->provider}"))->shared(), - ]; - } -} - -class ProviderIsUp -{ - // ... - - - public function middleware(): array - { - return [ - (new WithoutOverlapping("status:{$this->provider}"))->shared(), - ]; - } -} -``` - -### Throttling Exceptions - -Hypervel includes a `Hypervel\Queue\Middleware\ThrottlesExceptions` middleware that allows you to throttle exceptions. Once the job throws a given number of exceptions, all further attempts to execute the job are delayed until a specified time interval lapses. This middleware is particularly useful for jobs that interact with third-party services that are unstable. - -For example, let's imagine a queued job that interacts with a third-party API that begins throwing exceptions. To throttle exceptions, you can return the `ThrottlesExceptions` middleware from your job's `middleware` method. Typically, this middleware should be paired with a job that implements [time based attempts](#time-based-attempts): - -```php -use DateTime; -use Hypervel\Queue\Middleware\ThrottlesExceptions; - -/** - * Get the middleware the job should pass through. - * - * @return array - */ -public function middleware(): array -{ - return [new ThrottlesExceptions(10, 5)]; -} - -/** - * Determine the time at which the job should timeout. - */ -public function retryUntil(): DateTime -{ - return now()->addMinutes(5); -} -``` - -The first constructor argument accepted by the middleware is the number of exceptions the job can throw before being throttled, while the second constructor argument is the number of minutes that should elapse before the job is attempted again once it has been throttled. In the code example above, if the job throws 10 exceptions within 5 minutes, we will wait 5 minutes before attempting the job again. - -When a job throws an exception but the exception threshold has not yet been reached, the job will typically be retried immediately. However, you may specify the number of minutes such a job should be delayed by calling the `backoff` method when attaching the middleware to the job: - -```php -use Hypervel\Queue\Middleware\ThrottlesExceptions; - -/** - * Get the middleware the job should pass through. - * - * @return array - */ -public function middleware(): array -{ - return [(new ThrottlesExceptions(10, 5))->backoff(5)]; -} -``` - -Internally, this middleware uses Hypervel's cache system to implement rate limiting, and the job's class name is utilized as the cache "key". You may override this key by calling the `by` method when attaching the middleware to your job. This may be useful if you have multiple jobs interacting with the same third-party service and you would like them to share a common throttling "bucket": - -```php -use Hypervel\Queue\Middleware\ThrottlesExceptions; - -/** - * Get the middleware the job should pass through. - * - * @return array - */ -public function middleware(): array -{ - return [(new ThrottlesExceptions(10, 10))->by('key')]; -} -``` - -::: note -If you are using Redis, you may use the `Hypervel\Queue\Middleware\ThrottlesExceptionsWithRedis` middleware, which is fine-tuned for Redis and more efficient than the basic exception throttling middleware. -::: - -## Dispatching Jobs - -Once you have written your job class, you may dispatch it using the `dispatch` method on the job itself. The arguments passed to the `dispatch` method will be given to the job's constructor: - -```php -delay(now()->addMinutes(10)); - - return redirect('/podcasts'); - } -} -``` - -::: warning -The Amazon SQS queue service has a maximum delay time of 15 minutes. -::: - -#### Dispatching After the Response is Sent to the Browser - -Alternatively, the `dispatchAfterResponse` method delays dispatching a job until after the HTTP response is sent to the user's browser if your web server is using FastCGI. This will still allow the user to begin using the application even though a queued job is still executing. This should typically only be used for jobs that take about a second, such as sending an email. Since they are processed within the current HTTP request, jobs dispatched in this fashion do not require a queue worker to be running in order for them to be processed: - -```php -use App\Jobs\SendNotification; - -SendNotification::dispatchAfterResponse(); -``` - -You may also `dispatch` a closure and chain the `afterResponse` method onto the `dispatch` helper to execute a closure after the HTTP response has been sent to the browser: - -```php -use App\Mail\WelcomeMessage; -use Hypervel\Support\Facades\Mail; - -dispatch(function () { - Mail::to('taylor@example.com')->send(new WelcomeMessage); -})->afterResponse(); -``` - -### Synchronous Dispatching - -If you would like to dispatch a job immediately (synchronously), you may use the `dispatchSync` method. When using this method, the job will not be queued and will be executed immediately within the current process: - -```php - [ - 'driver' => 'redis', - // ... - 'after_commit' => true, -], -``` - -When the `after_commit` option is `true`, you may dispatch jobs within database transactions; however, Hypervel will wait until the open parent database transactions have been committed before actually dispatching the job. Of course, if no database transactions are currently open, the job will be dispatched immediately. - -If a transaction is rolled back due to an exception that occurs during the transaction, the jobs that were dispatched during that transaction will be discarded. - -::: note -Setting the `after_commit` configuration option to `true` will also cause any queued event listeners, mailables, notifications, and broadcast events to be dispatched after all open database transactions have been committed. -::: - -#### Specifying Commit Dispatch Behavior Inline - -If you do not set the `after_commit` queue connection configuration option to `true`, you may still indicate that a specific job should be dispatched after all open database transactions have been committed. To accomplish this, you may chain the `afterCommit` method onto your dispatch operation: - -```php -use App\Jobs\ProcessPodcast; - -ProcessPodcast::dispatch($podcast)->afterCommit(); -::: - -Likewise, if the `after_commit` configuration option is set to `true`, you may indicate that a specific job should be dispatched immediately without waiting for any open database transactions to commit: - -```php -ProcessPodcast::dispatch($podcast)->beforeCommit(); -``` - -### Job Chaining - -Job chaining allows you to specify a list of queued jobs that should be run in sequence after the primary job has executed successfully. If one job in the sequence fails, the rest of the jobs will not be run. To execute a queued job chain, you may use the `chain` method provided by the `Bus` facade. Hypervel's command bus is a lower level component that queued job dispatching is built on top of: - -```php -use App\Jobs\OptimizePodcast; -use App\Jobs\ProcessPodcast; -use App\Jobs\ReleasePodcast; -use Hypervel\Support\Facades\Bus; - -Bus::chain([ - new ProcessPodcast, - new OptimizePodcast, - new ReleasePodcast, -])->dispatch(); -``` - -In addition to chaining job class instances, you may also chain closures: - -```php -Bus::chain([ - new ProcessPodcast, - new OptimizePodcast, - function () { - Podcast::update(/* ... */); - }, -])->dispatch(); -``` - -::: warning -Deleting jobs using the `$this->delete()` method within the job will not prevent chained jobs from being processed. The chain will only stop executing if a job in the chain fails. -::: - -#### Chain Connection and Queue - -If you would like to specify the connection and queue that should be used for the chained jobs, you may use the `onConnection` and `onQueue` methods. These methods specify the queue connection and queue name that should be used unless the queued job is explicitly assigned a different connection / queue: - -```php -Bus::chain([ - new ProcessPodcast, - new OptimizePodcast, - new ReleasePodcast, -])->onConnection('redis')->onQueue('podcasts')->dispatch(); -``` - -#### Chain Failures - -When chaining jobs, you may use the `catch` method to specify a closure that should be invoked if a job within the chain fails. The given callback will receive the `Throwable` instance that caused the job failure: - -```php -use Hypervel\Support\Facades\Bus; -use Throwable; - -Bus::chain([ - new ProcessPodcast, - new OptimizePodcast, - new ReleasePodcast, -])->catch(function (Throwable $e) { - // A job within the chain has failed... -})->dispatch(); -``` - -::: warning -Since chain callbacks are serialized and executed at a later time by the Hypervel queue, you should not use the `$this` variable within chain callbacks. -::: - -### Customizing The Queue a Connection - -#### Dispatching to a Particular Queue - -By pushing jobs to different queues, you may "categorize" your queued jobs and even prioritize how many workers you assign to various queues. Keep in mind, this does not push jobs to different queue "connections" as defined by your queue configuration file, but only to specific queues within a single connection. To specify the queue, use the `onQueue` method when dispatching the job: - -```php -onQueue('processing'); - - return redirect('/podcasts'); - } -} -``` - -Alternatively, you may specify the job's queue by calling the `onQueue` method within the job's constructor: - -```php -onQueue('processing'); - } -} -``` - -#### Dispatching to a Particular Connection - -If your application interacts with multiple queue connections, you may specify which connection to push a job to using the `onConnection` method: - -```php -onConnection('sqs'); - - return redirect('/podcasts'); - } -} -``` - -You may chain the `onConnection` and `onQueue` methods together to specify the connection and the queue for a job: - -```php -ProcessPodcast::dispatch($podcast) - ->onConnection('sqs') - ->onQueue('processing'); -``` - -Alternatively, you may specify the job's connection by calling the `onConnection` method within the job's constructor: - -```php -onConnection('sqs'); - } -} -``` - -### Specifying Max Job Attempts / Timeout Values - -#### Max Attempts - -If one of your queued jobs is encountering an error, you likely do not want it to keep retrying indefinitely. Therefore, Hypervel provides various ways to specify how many times or for how long a job may be attempted. - -One approach to specifying the maximum number of times a job may be attempted is via the `--tries` switch on the Artisan command line. This will apply to all jobs processed by the worker unless the job being processed specifies the number of times it may be attempted: - -```shell:no-line-numbers -php artisan queue:work --tries=3 -``` - -If a job exceeds its maximum number of attempts, it will be considered a "failed" job. For more information on handling failed jobs, consult the [failed job documentation](#dealing-with-failed-jobs). If `--tries=0` is provided to the `queue:work` command, the job will be retried indefinitely. - -You may take a more granular approach by defining the maximum number of times a job may be attempted on the job class itself. If the maximum number of attempts is specified on the job, it will take precedence over the `--tries` value provided on the command line: - -```php -addMinutes(10); -} -``` - -::: note -You may also define a `tries` property or `retryUntil` method on your [queued event listeners](/docs/events#queued-event-listeners). -::: - -#### Max Exceptions - -Sometimes you may wish to specify that a job may be attempted many times, but should fail if the retries are triggered by a given number of unhandled exceptions (as opposed to being released by the `release` method directly). To accomplish this, you may define a `maxExceptions` property on your job class: - -```php -release(); -} -``` - -By default, the `release` method will release the job back onto the queue for immediate processing. However, you may instruct the queue to not make the job available for processing until a given number of seconds has elapsed by passing an integer or date instance to the `release` method: - -```php -$this->release(10); - -$this->release(now()->addSeconds(10)); -``` - -#### Manually Failing a Job - -Occasionally you may need to manually mark a job as "failed". To do so, you may call the `fail` method: - -```php -/** - * Execute the job. - */ -public function handle(): void -{ - // ... - - $this->fail(); -} -``` - -If you would like to mark your job as failed because of an exception that you have caught, you may pass the exception to the `fail` method. Or, for convenience, you may pass a string error message which will be converted to an exception for you: - -```php -$this->fail($exception); - -$this->fail('Something went wrong.'); -``` - -::: note -For more information on failed jobs, check out the [documentation on dealing with job failures](#dealing-with-failed-jobs). -::: - -## Job Batching - -Hypervel's job batching feature allows you to easily execute a batch of jobs and then perform some action when the batch of jobs has completed executing. Before getting started, you should create a database migration to build a table which will contain meta information about your job batches, such as their completion percentage. This migration may be generated using the `queue:batches-table` Artisan command: - -```shell:no-line-numbers -php artisan queue:batches-table - -php artisan migrate -``` - -### Defining Batchable Jobs - -To define a batchable job, you should [create a queueable job](#creating-jobs) as normal; however, you should add the `Hypervel\Bus\Batchable` trait to the job class. This trait provides access to a `batch` method which may be used to retrieve the current batch that the job is executing within: - -```php -batch()->cancelled()) { - // Determine if the batch has been cancelled... - - return; - } - - // Import a portion of the CSV file... - } -} -``` - -### Dispatching Batches - -To dispatch a batch of jobs, you should use the `batch` method of the `Bus` facade. Of course, batching is primarily useful when combined with completion callbacks. So, you may use the `then`, `catch`, and `finally` methods to define completion callbacks for the batch. Each of these callbacks will receive an `Hypervel\Bus\Batch` instance when they are invoked. In this example, we will imagine we are queueing a batch of jobs that each process a given number of rows from a CSV file: - -```php -use App\Jobs\ImportCsv; -use Hypervel\Bus\Batch; -use Hypervel\Support\Facades\Bus; -use Throwable; - -$batch = Bus::batch([ - new ImportCsv(1, 100), - new ImportCsv(101, 200), - new ImportCsv(201, 300), - new ImportCsv(301, 400), - new ImportCsv(401, 500), -])->before(function (Batch $batch) { - // The batch has been created but no jobs have been added... -})->progress(function (Batch $batch) { - // A single job has completed successfully... -})->then(function (Batch $batch) { - // All jobs completed successfully... -})->catch(function (Batch $batch, Throwable $e) { - // First batch job failure detected... -})->finally(function (Batch $batch) { - // The batch has finished executing... -})->dispatch(); - -return $batch->id; -``` - -The batch's ID, which may be accessed via the `$batch->id` property, may be used to [query the Hypervel command bus](#inspecting-batches) for information about the batch after it has been dispatched. - -::: warning -Since batch callbacks are serialized and executed at a later time by the Hypervel queue, you should not use the `$this` variable within the callbacks. -::: - -#### Naming Batches - -Some tools such as Hypervel Horizon and Hypervel Telescope may provide more user-friendly debug information for batches if batches are named. To assign an arbitrary name to a batch, you may call the `name` method while defining the batch: - -```php -$batch = Bus::batch([ - // ... -])->then(function (Batch $batch) { - // All jobs completed successfully... -})->name('Import CSV')->dispatch(); -``` - -#### Batch Connection and Queue - -If you would like to specify the connection and queue that should be used for the batched jobs, you may use the `onConnection` and `onQueue` methods. All batched jobs must execute within the same connection and queue: - -```php -$batch = Bus::batch([ - // ... -])->then(function (Batch $batch) { - // All jobs completed successfully... -})->onConnection('redis')->onQueue('imports')->dispatch(); -``` - -### Chains and Batches - -You may define a set of [chained jobs](#job-chaining) within a batch by placing the chained jobs within an array. For example, we may execute two job chains in parallel and execute a callback when both job chains have finished processing: - -```php -use App\Jobs\ReleasePodcast; -use App\Jobs\SendPodcastReleaseNotification; -use Hypervel\Bus\Batch; -use Hypervel\Support\Facades\Bus; - -Bus::batch([ - [ - new ReleasePodcast(1), - new SendPodcastReleaseNotification(1), - ], - [ - new ReleasePodcast(2), - new SendPodcastReleaseNotification(2), - ], -])->then(function (Batch $batch) { - // ... -})->dispatch(); -``` - -Conversely, you may run batches of jobs within a [chain](#job-chaining) by defining batches within the chain. For example, you could first run a batch of jobs to release multiple podcasts then a batch of jobs to send the release notifications: - -```php -use App\Jobs\FlushPodcastCache; -use App\Jobs\ReleasePodcast; -use App\Jobs\SendPodcastReleaseNotification; -use Hypervel\Support\Facades\Bus; - -Bus::chain([ - new FlushPodcastCache, - Bus::batch([ - new ReleasePodcast(1), - new ReleasePodcast(2), - ]), - Bus::batch([ - new SendPodcastReleaseNotification(1), - new SendPodcastReleaseNotification(2), - ]), -])->dispatch(); -``` - -### Adding Jobs to Batches - -Sometimes it may be useful to add additional jobs to a batch from within a batched job. This pattern can be useful when you need to batch thousands of jobs which may take too long to dispatch during a web request. So, instead, you may wish to dispatch an initial batch of "loader" jobs that hydrate the batch with even more jobs: - -```php -$batch = Bus::batch([ - new LoadImportBatch, - new LoadImportBatch, - new LoadImportBatch, -])->then(function (Batch $batch) { - // All jobs completed successfully... -})->name('Import Contacts')->dispatch(); -``` - -In this example, we will use the `LoadImportBatch` job to hydrate the batch with additional jobs. To accomplish this, we may use the `add` method on the batch instance that may be accessed via the job's `batch` method: - -```php -use App\Jobs\ImportContacts; -use Hypervel\Support\Collection; - -/** - * Execute the job. - */ -public function handle(): void -{ - if ($this->batch()->cancelled()) { - return; - } - - $this->batch()->add(Collection::times(1000, function () { - return new ImportContacts; - })); -} -``` - -::: warning -You may only add jobs to a batch from within a job that belongs to the same batch. -::: - -### Inspecting Batches - -The `Hypervel\Bus\Batch` instance that is provided to batch completion callbacks has a variety of properties and methods to assist you in interacting with and inspecting a given batch of jobs: - -```php -// The UUID of the batch... -$batch->id; - -// The name of the batch (if applicable)... -$batch->name; - -// The number of jobs assigned to the batch... -$batch->totalJobs; - -// The number of jobs that have not been processed by the queue... -$batch->pendingJobs; - -// The number of jobs that have failed... -$batch->failedJobs; - -// The number of jobs that have been processed thus far... -$batch->processedJobs(); - -// The completion percentage of the batch (0-100)... -$batch->progress(); - -// Indicates if the batch has finished executing... -$batch->finished(); - -// Cancel the execution of the batch... -$batch->cancel(); - -// Indicates if the batch has been cancelled... -$batch->cancelled(); -``` - -#### Returning Batches From Routes - -All `Hypervel\Bus\Batch` instances are JSON serializable, meaning you can return them directly from one of your application's routes to retrieve a JSON payload containing information about the batch, including its completion progress. This makes it convenient to display information about the batch's completion progress in your application's UI. - -To retrieve a batch by its ID, you may use the `Bus` facade's `findBatch` method: - -```php -use Hypervel\Support\Facades\Bus; -use Hypervel\Support\Facades\Route; - -Route::get('/batch/{batchId}', function (string $batchId) { - return Bus::findBatch($batchId); -}); -``` - -### Cancelling Batches - -Sometimes you may need to cancel a given batch's execution. This can be accomplished by calling the `cancel` method on the `Hypervel\Bus\Batch` instance: - -```php -/** - * Execute the job. - */ -public function handle(): void -{ - if ($this->user->exceedsImportLimit()) { - return $this->batch()->cancel(); - } - - if ($this->batch()->cancelled()) { - return; - } -} -``` - -As you may have noticed in the previous examples, batched jobs should typically determine if their corresponding batch has been cancelled before continuing execution. However, for convenience, you may assign the `SkipIfBatchCancelled` [middleware](#job-middleware) to the job instead. As its name indicates, this middleware will instruct Hypervel to not process the job if its corresponding batch has been cancelled: - -```php -use Hypervel\Queue\Middleware\SkipIfBatchCancelled; - -/** - * Get the middleware the job should pass through. - */ -public function middleware(): array -{ - return [new SkipIfBatchCancelled]; -} -``` - -### Batch Failures - -When a batched job fails, the `catch` callback (if assigned) will be invoked. This callback is only invoked for the first job that fails within the batch. - -#### Allowing Failures - -When a job within a batch fails, Hypervel will automatically mark the batch as "cancelled". If you wish, you may disable this behavior so that a job failure does not automatically mark the batch as cancelled. This may be accomplished by calling the `allowFailures` method while dispatching the batch: - -```php -$batch = Bus::batch([ - // ... -])->then(function (Batch $batch) { - // All jobs completed successfully... -})->allowFailures()->dispatch(); -``` - -#### Retrying Failed Batch Jobs - -For convenience, Hypervel provides a `queue:retry-batch` Artisan command that allows you to easily retry all of the failed jobs for a given batch. The `queue:retry-batch` command accepts the UUID of the batch whose failed jobs should be retried: - -```shell:no-line-numbers -php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5 -``` - -### Pruning Batches - -Without pruning, the `job_batches` table can accumulate records very quickly. To mitigate this, you should [schedule](/docs/scheduling) the `queue:prune-batches` Artisan command to run daily: - -```php -$schedule->command('queue:prune-batches')->daily(); -``` - -By default, all finished batches that are more than 24 hours old will be pruned. You may use the `hours` option when calling the command to determine how long to retain batch data. For example, the following command will delete all batches that finished over 48 hours ago: - -```php -$schedule->command('queue:prune-batches --hours=48')->daily(); -``` - -Sometimes, your `jobs_batches` table may accumulate batch records for batches that never completed successfully, such as batches where a job failed and that job was never retried successfully. You may instruct the `queue:prune-batches` command to prune these unfinished batch records using the `unfinished` option: - -```php -$schedule->command('queue:prune-batches --hours=48 --unfinished=72')->daily(); -``` - -Likewise, your `jobs_batches` table may also accumulate batch records for cancelled batches. You may instruct the `queue:prune-batches` command to prune these cancelled batch records using the `cancelled` option: - -```php -$schedule->command('queue:prune-batches --hours=48 --cancelled=72')->daily(); -``` - -### Storing Batches in DynamoDB - -Hypervel also provides support for storing batch meta information in [DynamoDB](https://aws.amazon.com/dynamodb) instead of a relational database. However, you will need to manually create a DynamoDB table to store all of the batch records. - -Typically, this table should be named `job_batches`, but you should name the table based on the value of the `queue.batching.table` configuration value within your application's `queue` configuration file. - -#### DynamoDB Batch Table Configuration - -The `job_batches` table should have a string primary partition key named `application` and a string primary sort key named `id`. The `application` portion of the key will contain your application's name as defined by the `name` configuration value within your application's `app` configuration file. Since the application name is part of the DynamoDB table's key, you can use the same table to store job batches for multiple Hypervel applications. - -In addition, you may define `ttl` attribute for your table if you would like to take advantage of [automatic batch pruning](#pruning-batches-in-dynamodb). - -#### DynamoDB Configuration - -Next, install the AWS SDK so that your Hypervel application can communicate with Amazon DynamoDB: - -```shell:no-line-numbers -composer require aws/aws-sdk-php -``` - -Then, set the `queue.batching.driver` configuration option's value to `dynamodb`. In addition, you should define `key`, `secret`, and `region` configuration options within the `batching` configuration array. These options will be used to authenticate with AWS. When using the `dynamodb` driver, the `queue.batching.database` configuration option is unnecessary: - -```php -'batching' => [ - 'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'), - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), - 'table' => 'job_batches', -], -``` - -#### Pruning Batches in DynamoDB - -When utilizing [DynamoDB](https://aws.amazon.com/dynamodb) to store job batch information, the typical pruning commands used to prune batches stored in a relational database will not work. Instead, you may utilize [DynamoDB's native TTL functionality](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) to automatically remove records for old batches. - -If you defined your DynamoDB table with a `ttl` attribute, you may define configuration parameters to instruct Hypervel how to prune batch records. The `queue.batching.ttl_attribute` configuration value defines the name of the attribute holding the TTL, while the `queue.batching.ttl` configuration value defines the number of seconds after which a batch record can be removed from the DynamoDB table, relative to the last time the record was updated: - -```php -'batching' => [ - 'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'), - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), - 'table' => 'job_batches', - 'ttl_attribute' => 'ttl', - 'ttl' => 60 * 60 * 24 * 7, // 7 days... -], -``` - -## Queueing Closures - -Instead of dispatching a job class to the queue, you may also dispatch a closure. This is great for quick, simple tasks that need to be executed outside of the current request cycle. When dispatching closures to the queue, the closure's code content is cryptographically signed so that it can not be modified in transit: - -```php -$podcast = App\Podcast::find(1); - -dispatch(function () use ($podcast) { - $podcast->publish(); -}); -``` - -Using the `catch` method, you may provide a closure that should be executed if the queued closure fails to complete successfully after exhausting all of your queue's [configured retry attempts](#max-job-attempts-and-timeout): - -```php -use Throwable; - -dispatch(function () use ($podcast) { - $podcast->publish(); -})->catch(function (Throwable $e) { - // This job has failed... -}); -``` - -::: warning -Since `catch` callbacks are serialized and executed at a later time by the Hypervel queue, you should not use the `$this` variable within `catch` callbacks. -::: - -## Running the Queue Worker - -### The `queue:work` Command - -Hypervel includes an Artisan command that will start a queue worker and process new jobs as they are pushed onto the queue. You may run the worker using the `queue:work` Artisan command. Note that once the `queue:work` command has started, it will continue to run until it is manually stopped or you close your terminal: - -```shell:no-line-numbers -php artisan queue:work -``` - -::: note -To keep the `queue:work` process running permanently in the background, you should use a process monitor such as [Supervisor](#supervisor-configuration) to ensure that the queue worker does not stop running. -::: - -Remember, queue workers are long-lived processes and store the booted application state in memory. As a result, they will not notice changes in your code base after they have been started. So, during your deployment process, be sure to [restart your queue workers](#queue-workers-and-deployment). In addition, remember that any static state created or modified by your application will not be automatically reset between jobs. - -Alternatively, you may run the `queue:listen` command. When using the `queue:listen` command, you don't have to manually restart the worker when you want to reload your updated code or reset the application state; however, this command is significantly less efficient than the `queue:work` command: - -```shell:no-line-numbers -php artisan queue:listen -``` - -#### Running Multiple Queue Workers - -To assign multiple workers to a queue and process jobs concurrently, you should simply start multiple `queue:work` processes. This can either be done locally via multiple tabs in your terminal or in production using your process manager's configuration settings. [When using Supervisor](#supervisor-configuration), you may use the `numprocs` configuration value. - -#### Concurrent Processing in Single Queue Worker - -Queue workers in Hypervel support concurrent processing using coroutines. For I/O-bound queue jobs, you can simply utilize the concurrency option to execute multiple jobs simultaneously within a single worker. - -```shell:no-line-numbers -php artisan queue:work --concurrency=10 -``` - -In this example, you maintain just one worker process while running 10 coroutines that process jobs concurrently. Traditional Laravel requires scaling by increasing the number of worker processes, but Hypervel's coroutine implementation delivers significantly higher efficiency with minimal resource overhead. - -::: note -For CPU-bound queue jobs, coroutines offer limited performance benefits. To maximize CPU core utilization, you should still run multiple worker processes to effectively distribute computational workload across your available CPU resources. -::: - -#### Specifying the Connection and Queue - -You may also specify which queue connection the worker should utilize. The connection name passed to the `work` command should correspond to one of the connections defined in your `config/queue.php` configuration file: - -```shell:no-line-numbers -php artisan queue:work redis -``` - -By default, the `queue:work` command only processes jobs for the default queue on a given connection. However, you may customize your queue worker even further by only processing particular queues for a given connection. For example, if all of your emails are processed in an `emails` queue on your `redis` queue connection, you may issue the following command to start a worker that only processes that queue: - -```shell:no-line-numbers -php artisan queue:work redis --queue=emails -``` - -#### Processing a Specified Number of Jobs - -The `--once` option may be used to instruct the worker to only process a single job from the queue: - -```shell:no-line-numbers -php artisan queue:work --once -``` - -The `--max-jobs` option may be used to instruct the worker to process the given number of jobs and then exit. This option may be useful when combined with [Supervisor](#supervisor-configuration) so that your workers are automatically restarted after processing a given number of jobs, releasing any memory they may have accumulated: - -```shell:no-line-numbers -php artisan queue:work --max-jobs=1000 -``` - -#### Processing All Queued Jobs and Then Exiting - -The `--stop-when-empty` option may be used to instruct the worker to process all jobs and then exit gracefully. This option can be useful when processing Hypervel queues within a Docker container if you wish to shutdown the container after the queue is empty: - -```shell:no-line-numbers -php artisan queue:work --stop-when-empty -``` - -#### Processing Jobs for a Given Number of Seconds - -The `--max-time` option may be used to instruct the worker to process jobs for the given number of seconds and then exit. This option may be useful when combined with [Supervisor](#supervisor-configuration) so that your workers are automatically restarted after processing jobs for a given amount of time, releasing any memory they may have accumulated: - -```shell:no-line-numbers -# Process jobs for one hour and then exit... -php artisan queue:work --max-time=3600 -``` - -#### Worker Sleep Duration - -When jobs are available on the queue, the worker will keep processing jobs with no delay in between jobs. However, the `sleep` option determines how many seconds the worker will "sleep" if there are no jobs available. Of course, while sleeping, the worker will not process any new jobs: - -```shell:no-line-numbers -php artisan queue:work --sleep=3 -``` - -#### Maintenance Mode and Queues - -While your application is in [maintenance mode](/docs/configuration#maintenance-mode), no queued jobs will be handled. The jobs will continue to be handled as normal once the application is out of maintenance mode. - -To force your queue workers to process jobs even if maintenance mode is enabled, you may use `--force` option: - -```shell:no-line-numbers -php artisan queue:work --force -``` - -#### Resource Considerations - -Daemon queue workers do not "reboot" the framework before processing each job. Therefore, you should release any heavy resources after each job completes. For example, if you are doing image manipulation with the GD library, you should free the memory with `imagedestroy` when you are done processing the image. - -### Queue Priorities - -Sometimes you may wish to prioritize how your queues are processed. For example, in your `config/queue.php` configuration file, you may set the default `queue` for your `redis` connection to `low`. However, occasionally you may wish to push a job to a `high` priority queue like so: - -```php -dispatch((new Job)->onQueue('high')); -``` - -To start a worker that verifies that all of the `high` queue jobs are processed before continuing to any jobs on the `low` queue, pass a comma-delimited list of queue names to the `work` command: - -```shell:no-line-numbers -php artisan queue:work --queue=high,low -``` - -### Queue Workers and Deployment - -Since queue workers are long-lived processes, they will not notice changes to your code without being restarted. So, the simplest way to deploy an application using queue workers is to restart the workers during your deployment process. You may gracefully restart all of the workers by issuing the `queue:restart` command: - -```shell:no-line-numbers -php artisan queue:restart -``` - -This command will instruct all queue workers to gracefully exit after they finish processing their current job so that no existing jobs are lost. Since the queue workers will exit when the `queue:restart` command is executed, you should be running a process manager such as [Supervisor](#supervisor-configuration) to automatically restart the queue workers. - -::: note -The queue uses the [cache](/docs/cache) to store restart signals, so you should verify that a cache driver is properly configured for your application before using this feature. -::: - -### Job Expirations and Timeouts - -#### Job Expiration - -In your `config/queue.php` configuration file, each queue connection defines a `retry_after` option. This option specifies how many seconds the queue connection should wait before retrying a job that is being processed. For example, if the value of `retry_after` is set to `90`, the job will be released back onto the queue if it has been processing for 90 seconds without being released or deleted. Typically, you should set the `retry_after` value to the maximum number of seconds your jobs should reasonably take to complete processing. - -::: warning -The only queue connection which does not contain a `retry_after` value is Amazon SQS. SQS will retry the job based on the [Default Visibility Timeout](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/AboutVT.html) which is managed within the AWS console. -::: - -#### Worker Timeouts - -The `queue:work` Artisan command exposes a `--timeout` option. By default, the `--timeout` value is 60 seconds. If a job is processing for longer than the number of seconds specified by the timeout value, the worker processing the job will exit with an error. Typically, the worker will be restarted automatically by a [process manager configured on your server](#supervisor-configuration): - -```shell:no-line-numbers -php artisan queue:work --timeout=60 -``` - -The `retry_after` configuration option and the `--timeout` CLI option are different, but work together to ensure that jobs are not lost and that jobs are only successfully processed once. - -::: warning -The `--timeout` value should always be at least several seconds shorter than your `retry_after` configuration value. This will ensure that a worker processing a frozen job is always terminated before the job is retried. If your `--timeout` option is longer than your `retry_after` configuration value, your jobs may be processed twice. -::: - -## Supervisor Configuration - -In production, you need a way to keep your `queue:work` processes running. A `queue:work` process may stop running for a variety of reasons, such as an exceeded worker timeout or the execution of the `queue:restart` command. - -For this reason, you need to configure a process monitor that can detect when your `queue:work` processes exit and automatically restart them. In addition, process monitors can allow you to specify how many `queue:work` processes you would like to run concurrently. Supervisor is a process monitor commonly used in Linux environments and we will discuss how to configure it in the following documentation. - -#### Installing Supervisor - -Supervisor is a process monitor for the Linux operating system, and will automatically restart your `queue:work` processes if they fail. To install Supervisor on Ubuntu, you may use the following command: - -```shell:no-line-numbers -sudo apt-get install supervisor -``` - -#### Configuring Supervisor - -Supervisor configuration files are typically stored in the `/etc/supervisor/conf.d` directory. Within this directory, you may create any number of configuration files that instruct supervisor how your processes should be monitored. For example, let's create a `laravel-worker.conf` file that starts and monitors `queue:work` processes: - -```ini -[program:laravel-worker] -process_name=%(program_name)s_%(process_num)02d -command=php /home/forge/app.com/artisan queue:work sqs --sleep=3 --tries=3 --max-time=3600 -autostart=true -autorestart=true -stopasgroup=true -killasgroup=true -user=forge -numprocs=8 -redirect_stderr=true -stdout_logfile=/home/forge/app.com/worker.log -stopwaitsecs=3600 -``` - -In this example, the `numprocs` directive will instruct Supervisor to run eight `queue:work` processes and monitor all of them, automatically restarting them if they fail. You should change the `command` directive of the configuration to reflect your desired queue connection and worker options. - -::: warning -You should ensure that the value of `stopwaitsecs` is greater than the number of seconds consumed by your longest running job. Otherwise, Supervisor may kill the job before it is finished processing. -::: - -#### Starting Supervisor - -Once the configuration file has been created, you may update the Supervisor configuration and start the processes using the following commands: - -```shell:no-line-numbers -sudo supervisorctl reread - -sudo supervisorctl update - -sudo supervisorctl start "laravel-worker:*" -``` - -For more information on Supervisor, consult the [Supervisor documentation](http://supervisord.org/index.html). - -## Dealing With Failed Jobs - -Sometimes your queued jobs will fail. Don't worry, things don't always go as planned! Hypervel includes a convenient way to [specify the maximum number of times a job should be attempted](#max-job-attempts-and-timeout). After an asynchronous job has exceeded this number of attempts, it will be inserted into the `failed_jobs` database table. [Synchronously dispatched jobs](/docs/queues#synchronous-dispatching) that fail are not stored in this table and their exceptions are immediately handled by the application. - -A migration to create the `failed_jobs` table is typically already present in new Hypervel applications. However, if your application does not contain a migration for this table, you may use the `queue:failed-table` command to create the migration: - -```shell:no-line-numbers -php artisan queue:failed-table - -php artisan migrate -``` - -When running a [queue worker](#running-the-queue-worker) process, you may specify the maximum number of times a job should be attempted using the `--tries` switch on the `queue:work` command. If you do not specify a value for the `--tries` option, jobs will only be attempted once or as many times as specified by the job class' `$tries` property: - -```shell:no-line-numbers -php artisan queue:work redis --tries=3 -``` - -Using the `--backoff` option, you may specify how many seconds Hypervel should wait before retrying a job that has encountered an exception. By default, a job is immediately released back onto the queue so that it may be attempted again: - -```shell:no-line-numbers -php artisan queue:work redis --tries=3 --backoff=3 -``` - -If you would like to configure how many seconds Hypervel should wait before retrying a job that has encountered an exception on a per-job basis, you may do so by defining a `backoff` property on your job class: - -```php -/** - * The number of seconds to wait before retrying the job. - */ -public int $backoff = 3; -``` - -If you require more complex logic for determining the job's backoff time, you may define a `backoff` method on your job class: - -```php -/** -* Calculate the number of seconds to wait before retrying the job. -*/ -public function backoff(): int -{ - return 3; -} -``` - -You may easily configure "exponential" backoffs by returning an array of backoff values from the `backoff` method. In this example, the retry delay will be 1 second for the first retry, 5 seconds for the second retry, 10 seconds for the third retry, and 10 seconds for every subsequent retry if there are more attempts remaining: - -```php -/** -* Calculate the number of seconds to wait before retrying the job. -* -* @return array -*/ -public function backoff(): array -{ - return [1, 5, 10]; -} -``` - -### Cleaning Up After Failed Jobs - -When a particular job fails, you may want to send an alert to your users or revert any actions that were partially completed by the job. To accomplish this, you may define a `failed` method on your job class. The `Throwable` instance that caused the job to fail will be passed to the `failed` method: - -```php - [ - 'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'), - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), - 'table' => 'failed_jobs', -], -``` - -### Disabling Failed Job Storage - -You may instruct Hypervel to discard failed jobs without storing them by setting the `queue.failed.driver` configuration option's value to `null`. Typically, this may be accomplished via the `QUEUE_FAILED_DRIVER` environment variable: - -```ini -QUEUE_FAILED_DRIVER=null -``` - -### Failed Job Events - -If you would like to register an event listener that will be invoked when a job fails, you may use the `Queue` facade's `failing` method. For example, we may attach a closure to this event from the `boot` method of the `AppServiceProvider` that is included with Laravel: - -```php -connectionName - // $event->job - // $event->exception - }); - } -} -``` - -## Clearing Jobs From Queues - -If you would like to delete all jobs from the default queue of the default connection, you may do so using the `queue:clear` Artisan command: - -```shell:no-line-numbers -php artisan queue:clear -``` - -You may also provide the `connection` argument and `queue` option to delete jobs from a specific connection and queue: - -```shell:no-line-numbers -php artisan queue:clear redis --queue=emails -``` - -::: warning -Clearing jobs from queues is only available for the SQS, Redis, and database queue drivers. In addition, the SQS message deletion process takes up to 60 seconds, so jobs sent to the SQS queue up to 60 seconds after you clear the queue might also be deleted. -::: - -## Monitoring Your Queues - -If your queue receives a sudden influx of jobs, it could become overwhelmed, leading to a long wait time for jobs to complete. If you wish, Hypervel can alert you when your queue job count exceeds a specified threshold. - -To get started, you should schedule the `queue:monitor` command to [run every minute](/docs/scheduling). The command accepts the names of the queues you wish to monitor as well as your desired job count threshold: - -```shell:no-line-numbers -php artisan queue:monitor redis:default,redis:deployments --max=100 -``` - -Scheduling this command alone is not enough to trigger a notification alerting you of the queue's overwhelmed status. When the command encounters a queue that has a job count exceeding your threshold, an `Hypervel\Queue\Events\QueueBusy` event will be dispatched. You may listen for this event within your application's `EventServiceProvider` in order to send a notification to you or your development team: - -```php -use App\Notifications\QueueHasLongWaitTime; -use Hypervel\Queue\Events\QueueBusy; -use Hypervel\Support\Facades\Event; -use Hypervel\Support\Facades\Notification; - -/** - * Register any other events for your application. - */ -public function boot(): void -{ - Event::listen(function (QueueBusy $event) { - Notification::route('mail', 'dev@example.com') - ->notify(new QueueHasLongWaitTime( - $event->connection, - $event->queue, - $event->size - )); - }); -} -``` - -## Testing - -When testing code that dispatches jobs, you may wish to instruct Hypervel to not actually execute the job itself, since the job's code can be tested directly and separately of the code that dispatches it. Of course, to test the job itself, you may instantiate a job instance and invoke the `handle` method directly in your test. - -You may use the `Queue` facade's `fake` method to prevent queued jobs from actually being pushed to the queue. After calling the `Queue` facade's `fake` method, you may then assert that the application attempted to push jobs to the queue: - -```php -order->id === $order->id; -}); -``` - -### Faking a Subset of Jobs - -If you only need to fake specific jobs while allowing your other jobs to execute normally, you may pass the class names of the jobs that should be faked to the `fake` method: - -```php -public function test_orders_can_be_shipped(): void -{ - Queue::fake([ - ShipOrder::class, - ]); - - // Perform order shipping... - - // Assert a job was pushed twice... - Queue::assertPushed(ShipOrder::class, 2); -} -``` - -You may fake all jobs except for a set of specified jobs using the `except` method: - -```php -Queue::fake()->except([ - ShipOrder::class, -]); -``` - -### Testing Job Chains - -To test job chains, you will need to utilize the `Bus` facade's faking capabilities. The `Bus` facade's `assertChained` method may be used to assert that a [chain of jobs](/docs/queues#job-chaining) was dispatched. The `assertChained` method accepts an array of chained jobs as its first argument: - -```php -use App\Jobs\RecordShipment; -use App\Jobs\ShipOrder; -use App\Jobs\UpdateInventory; -use Hypervel\Support\Facades\Bus; - -Bus::fake(); - -// ... - -Bus::assertChained([ - ShipOrder::class, - RecordShipment::class, - UpdateInventory::class -]); -``` - -As you can see in the example above, the array of chained jobs may be an array of the job's class names. However, you may also provide an array of actual job instances. When doing so, Hypervel will ensure that the job instances are of the same class and have the same property values of the chained jobs dispatched by your application: - -```php -Bus::assertChained([ - new ShipOrder, - new RecordShipment, - new UpdateInventory, -]); -``` - -You may use the `assertDispatchedWithoutChain` method to assert that a job was pushed without a chain of jobs: - -```php -Bus::assertDispatchedWithoutChain(ShipOrder::class); -``` - -#### Testing Chained Batches - -If your job chain [contains a batch of jobs](#chains-and-batches), you may assert that the chained batch matches your expectations by inserting a `Bus::chainedBatch` definition within your chain assertion: - -```php -use App\Jobs\ShipOrder; -use App\Jobs\UpdateInventory; -use Hypervel\Bus\PendingBatch; -use Hypervel\Support\Facades\Bus; - -Bus::assertChained([ - new ShipOrder, - Bus::chainedBatch(function (PendingBatch $batch) { - return $batch->jobs->count() === 3; - }), - new UpdateInventory, -]); -``` - -### Testing Job Batches - -The `Bus` facade's `assertBatched` method may be used to assert that a [batch of jobs](/docs/queues#job-batching) was dispatched. The closure given to the `assertBatched` method receives an instance of `Hypervel\Bus\PendingBatch`, which may be used to inspect the jobs within the batch: - -```php -use Hypervel\Bus\PendingBatch; -use Hypervel\Support\Facades\Bus; - -Bus::fake(); - -// ... - -Bus::assertBatched(function (PendingBatch $batch) { - return $batch->name == 'import-csv' && - $batch->jobs->count() === 10; -}); -``` - -You may use the `assertBatchCount` method to assert that a given number of batches were dispatched: - -```php -Bus::assertBatchCount(3); -``` - -You may use `assertNothingBatched` to assert that no batches were dispatched: - -```php -Bus::assertNothingBatched(); -``` - -#### Testing Job / Batch Interaction - -In addition, you may occasionally need to test an individual job's interaction with its underlying batch. For example, you may need to test if a job cancelled further processing for its batch. To accomplish this, you need to assign a fake batch to the job via the `withFakeBatch` method. The `withFakeBatch` method returns a tuple containing the job instance and the fake batch: - -```php -[$job, $batch] = (new ShipOrder)->withFakeBatch(); - -$job->handle(); - -$this->assertTrue($batch->cancelled()); -$this->assertEmpty($batch->added); -``` - -## Job Events - -Using the `before` and `after` methods on the `Queue` [facade](/docs/facades), you may specify callbacks to be executed before or after a queued job is processed. These callbacks are a great opportunity to perform additional logging or increment statistics for a dashboard. Typically, you should call these methods from the `boot` method of a [service provider](/docs/providers). For example, we may use the `AppServiceProvider` that is included with Laravel: - -```php -connectionName - // $event->job - // $event->job->payload() - }); - - Queue::after(function (JobProcessed $event) { - // $event->connectionName - // $event->job - // $event->job->payload() - }); - } -} -``` - -Using the `looping` method on the `Queue` [facade](/docs/facades), you may specify callbacks that execute before the worker attempts to fetch a job from a queue. For example, you might register a closure to rollback any transactions that were left open by a previously failed job: - -```php -use Hyperf\Database\ConnectionResolverInterface; -use Hypervel\Support\Facades\DB; -use Hypervel\Support\Facades\Queue; - -Queue::looping(function () { - $transactionLevel = $this->container - ->get(ConnectionResolverInterface::class) - ->connection($event->connectionName) - ->transactionLevel() - while ($transactionLevel > 0) { - DB::rollBack(); - } -}); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/rate-limiting.md b/_archive/docs/0.3/docs/rate-limiting.md deleted file mode 100644 index 0da155090e..0000000000 --- a/_archive/docs/0.3/docs/rate-limiting.md +++ /dev/null @@ -1,131 +0,0 @@ -# Rate Limiting -[[toc]] - -## Introduction - -Hypervel includes a simple to use rate limiting abstraction which, in conjunction with your application's [cache](cache), provides an easy way to limit any action during a specified window of time. - -::: note -If you are interested in rate limiting incoming HTTP requests, please consult the [rate limiter middleware documentation](routing#rate-limiting). -::: - -### Cache Configuration - -Typically, the rate limiter utilizes your default application cache as defined by the `default` key within your application's `cache` configuration file. However, you may specify which cache driver the rate limiter should use by defining a `limiter` key within your application's `cache` configuration file: - - -```php -'default' => 'memcached', - -'limiter' => 'redis', -``` - -## Basic Usage - -The `Hypervel\Support\Facades\RateLimiter` facade may be used to interact with the rate limiter. The simplest method offered by the rate limiter is the `attempt` method, which rate limits a given callback for a given number of seconds. - -The `attempt` method returns `false` when the callback has no remaining attempts available; otherwise, the `attempt` method will return the callback's result or `true`. The first argument accepted by the `attempt` method is a rate limiter "key", which may be any string of your choosing that represents the action being rate limited: - -```php -use Hypervel\Support\Facades\RateLimiter; - -$executed = RateLimiter::attempt( - 'send-message:'.$user->id, - $perMinute = 5, - function() { - // Send message... - } -); - -if (! $executed) { - return 'Too many messages sent!'; -} -``` - -If necessary, you may provide a fourth argument to the `attempt` method, which is the "decay rate", or the number of seconds until the available attempts are reset. For example, we can modify the example above to allow five attempts every two minutes: - -```php -$executed = RateLimiter::attempt( - 'send-message:'.$user->id, - $perTwoMinutes = 5, - function() { - // Send message... - }, - $decayRate = 120, -); -``` - -### Manually Incrementing Attempts - -If you would like to manually interact with the rate limiter, a variety of other methods are available. For example, you may invoke the `tooManyAttempts` method to determine if a given rate limiter key has exceeded its maximum number of allowed attempts per minute: - -```php -use Hypervel\Support\Facades\RateLimiter; - -if (RateLimiter::tooManyAttempts('send-message:'.$user->id, $perMinute = 5)) { - return 'Too many attempts!'; -} - -RateLimiter::increment('send-message:'.$user->id); - -// Send message... -``` - -Alternatively, you may use the `remaining` method to retrieve the number of attempts remaining for a given key. If a given key has retries remaining, you may invoke the `increment` method to increment the number of total attempts: - - -```php -use Hypervel\Support\Facades\RateLimiter; - -if (RateLimiter::remaining('send-message:'.$user->id, $perMinute = 5)) { - RateLimiter::increment('send-message:'.$user->id); - - // Send message... -} -``` - -If you would like to increment the value for a given rate limiter key by more than one, you may provide the desired amount to the `increment` method: - -```php -RateLimiter::increment('send-message:'.$user->id, amount: 5); -``` - -#### Determining Limiter Availability - -When a key has no more attempts left, the `availableIn` method returns the number of seconds remaining until more attempts will be available: - - -```php -use Hypervel\Support\Facades\RateLimiter; - -if (RateLimiter::tooManyAttempts('send-message:'.$user->id, $perMinute = 5)) { - $seconds = RateLimiter::availableIn('send-message:'.$user->id); - - return 'You may try again in '.$seconds.' seconds.'; -} - -RateLimiter::increment('send-message:'.$user->id); - -// Send message... -``` - -### Clearing Attempts - -You may reset the number of attempts for a given rate limiter key using the `clear` method. For example, you may reset the number of attempts when a given message is read by the receiver: - -```php -use App\Models\Message; -use Hypervel\Support\Facades\RateLimiter; - -/** - * Mark the message as read. - */ -public function read(Message $message): Message -{ - $message->markAsRead(); - - RateLimiter::clear('send-message:'.$message->user_id); - - return $message; -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/redis.md b/_archive/docs/0.3/docs/redis.md deleted file mode 100644 index 97a825ce22..0000000000 --- a/_archive/docs/0.3/docs/redis.md +++ /dev/null @@ -1,333 +0,0 @@ -# Redis -[[toc]] - -## Introduction - -[Redis](https://redis.io) is an open source, advanced key-value store. It is often referred to as a data structure server since keys can contain [strings](https://redis.io/docs/data-types/strings/), [hashes](https://redis.io/docs/data-types/hashes/), [lists](https://redis.io/docs/data-types/lists/), [sets](https://redis.io/docs/data-types/sets/), and [sorted sets](https://redis.io/docs/data-types/sorted-sets/). - -Before using Redis with Hypervel Hyperf, you need to install and use the [PhpRedis](https://github.com/phpredis/phpredis) PHP extension via PECL. - -::: warning -Unlike in Laravel, Hypervel doesn't support `predis` as an alternative for connection driver. -::: - -## Configuration - -You may configure your application's Redis settings via the `config/database.php` configuration file. Within this file, you will see a `redis` array containing the Redis servers utilized by your application: - -```php -'redis' => [ - 'options' => [ - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'hypervel'), '_') . '_database_'), - ], - - 'default' => [ - 'host' => env('REDIS_HOST', 'localhost'), - 'password' => env('REDIS_PASSWORD'), - 'port' => (int) env('REDIS_PORT', 6379), - 'database' => (int) env('REDIS_DB', 0), - ], - - 'queue' => [ - 'host' => env('REDIS_HOST', 'localhost'), - 'password' => env('REDIS_PASSWORD'), - 'port' => (int) env('REDIS_PORT', 6379), - 'database' => (int) env('REDIS_DB', 0), - ], -], -``` - -Each Redis server defined in your configuration file is required to have a name, host, and a port for the Redis connection. - -### Clusters - -If your application is utilizing a cluster of Redis servers, you should define these clusters within a `clusters` key of your Redis configuration. This configuration key does not exist by default so you will need to create it within your application's `config/database.php` configuration file: - -```php -'redis' => [ - 'default' => [ - 'host' => env('REDIS_HOST', 'localhost'), - 'password' => env('REDIS_PASSWORD'), - 'port' => (int) env('REDIS_PORT', 6379), - 'database' => (int) env('REDIS_DB', 0), - 'cluster' => [ - 'enable' => true, - 'name' => 'mycluster', - 'seeds' => [], - ], - ], - // ... -], -``` - -You can also configure `seeds` in `cluster` option without filling name for the cluster, like: - -```php -'cluster' => [ - 'enable' => true, - 'name' => null, - 'seeds' => [ - '192.168.1.110:6379', - '192.168.1.111:6379', - ], -``` - -### Sentinel - -Redis Sentinel provides high availability for Redis by implementing monitoring, notifications, automatic failover, and configuration provider. Sentinel helps manage your Redis deployment by constantly checking if your master and replica instances are working as expected. - -To configure Redis Sentinel in your Hypervel application, you can use the following configuration in your `config/database.php`: - -```php -'redis' => [ - 'default' => [ - 'sentinel' => [ - 'enable' => (bool) env('REDIS_SENTINEL_ENABLE', false), - 'master_name' => env('REDIS_MASTER_NAME', 'mymaster'), - 'nodes' => explode(';', env('REDIS_SENTINEL_NODE', '')), - 'persistent' => false, - 'read_timeout' => 30.0, - 'auth' => env('REDIS_SENTINEL_PASSWORD', ''), - ], -], -``` - -When Sentinel is enabled, Hypervel will automatically handle failover scenarios by connecting to the current Redis master node as determined by the Sentinel cluster. - -### Connection Pooling - -Hypervel implements connection pooling for Redis to efficiently manage and reuse Redis connections. Connection pooling helps improve performance by maintaining a pool of pre-established connections that can be reused across requests, rather than creating and destroying connections for each operation. This reduces connection overhead and improves response times, especially in high-concurrency scenarios. - - -```php -'redis' => [ - 'default' => [ - 'pool' => [ - 'min_connections' => 1, - 'max_connections' => 10, - 'connect_timeout' => 10.0, - 'wait_timeout' => 3.0, - 'heartbeat' => -1, - 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), - ], -], -``` - -### Other Configuration - -In addition to the default configuration options, PhpRedis supports the following additional connection parameters: `name`, `persistent`, `persistent_id`, `prefix`, `read_timeout`, `retry_interval`, `max_retries`, `backoff_algorithm`, `backoff_base`, `backoff_cap`, `timeout`, and `context`. You may add any of these options to your Redis server configuration in the `config/database.php` configuration file: - -```php -'default' => [ - 'url' => env('REDIS_URL'), - 'host' => env('REDIS_HOST', '127.0.0.1'), - 'username' => env('REDIS_USERNAME'), - 'password' => env('REDIS_PASSWORD'), - 'port' => env('REDIS_PORT', '6379'), - 'database' => env('REDIS_DB', '0'), - 'read_timeout' => 60, - 'context' => [ - // 'auth' => ['username', 'secret'], - // 'stream' => ['verify_peer' => false], - ], -], -``` - -#### PhpRedis Serialization and Compression - -The PhpRedis extension may also be configured to use a variety of serializers and compression algorithms. These algorithms can be configured via the `options` array of your Redis configuration: - -```php -'redis' => [ - 'options' => [ - 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'hypervel'), '_').'_database_'), - 'serializer' => Redis::SERIALIZER_MSGPACK, - 'compression' => Redis::COMPRESSION_LZ4, - ], - // ... -], -``` - -Currently supported serializers include: `Redis::SERIALIZER_NONE` (default), `Redis::SERIALIZER_PHP`, `Redis::SERIALIZER_JSON`, `Redis::SERIALIZER_IGBINARY`, and `Redis::SERIALIZER_MSGPACK`. - -Supported compression algorithms include: `Redis::COMPRESSION_NONE` (default), `Redis::COMPRESSION_LZF`, `Redis::COMPRESSION_ZSTD`, and `Redis::COMPRESSION_LZ4`. - -## Interacting With Redis - -You may interact with Redis by calling various methods on the `Redis` [facade](/docs/facades). The `Redis` facade supports dynamic methods, meaning you may call any [Redis command](https://redis.io/commands) on the facade and the command will be passed directly to Redis. In this example, we will call the Redis `GET` command by calling the `get` method on the `Redis` facade: - -```php - Redis::get('user:profile:'.$id) - ]); - } -} -``` - -As mentioned above, you may call any of Redis' commands on the `Redis` facade. Hypervel uses magic methods to pass the commands to the Redis server. If a Redis command expects arguments, you should pass those to the facade's corresponding method: - -```php -use Hypervel\Support\Facades\Redis; - -Redis::set('name', 'Taylor'); - -$values = Redis::lrange('names', 5, 10); -``` - -Alternatively, you may pass commands to the server using the `Redis` facade's `command` method, which accepts the name of the command as its first argument and an array of values as its second argument: - -```php -$values = Redis::command('lrange', ['name', 5, 10]); -``` - -#### Using Multiple Redis Connections - -Your application's `config/database.php` configuration file allows you to define multiple Redis connections / servers. You may obtain a connection to a specific Redis connection using the `Redis` facade's `connection` method: - -```php -$redis = Redis::connection('connection-name'); -``` - -To obtain an instance of the default Redis connection, you may call the `connection` method without any additional arguments: - -```php -$redis = Redis::connection(); -``` - -### Transactions - -The `Redis` facade's `transaction` method provides a convenient wrapper around Redis' native `MULTI` and `EXEC` commands. The `transaction` method accepts a closure as its only argument. This closure will receive a Redis connection instance and may issue any commands it would like to this instance. All of the Redis commands issued within the closure will be executed in a single, atomic transaction: - -```php -use Redis; -use Hypervel\Support\Facades; - -Facades\Redis::transaction(function (Redis $redis) { - $redis->incr('user_visits', 1); - $redis->incr('total_visits', 1); -}); -``` - -::: warning -When defining a Redis transaction, you may not retrieve any values from the Redis connection. Remember, your transaction is executed as a single, atomic operation and that operation is not executed until your entire closure has finished executing its commands. -::: - -#### Lua Scripts - -The `eval` method provides another method of executing multiple Redis commands in a single, atomic operation. However, the `eval` method has the benefit of being able to interact with and inspect Redis key values during that operation. Redis scripts are written in the [Lua programming language](https://www.lua.org). - -The `eval` method can be a bit scary at first, but we'll explore a basic example to break the ice. The `eval` method expects several arguments. First, you should pass the Lua script (as a string) to the method. Secondly, you should pass the number of keys (as an integer) that the script interacts with. Thirdly, you should pass the names of those keys. Finally, you may pass any other additional arguments that you need to access within your script. - -In this example, we will increment a counter, inspect its new value, and increment a second counter if the first counter's value is greater than five. Finally, we will return the value of the first counter: - -```php -$value = Redis::eval(<<<'LUA' - local counter = redis.call("incr", KEYS[1]) - - if counter > 5 then - redis.call("incr", KEYS[2]) - end - - return counter -LUA, 2, 'first-counter', 'second-counter'); -``` - -::: warning -Please consult the [Redis documentation](https://redis.io/commands/eval) for more information on Redis scripting. -::: - -### Pipelining Commands - -Sometimes you may need to execute dozens of Redis commands. Instead of making a network trip to your Redis server for each command, you may use the `pipeline` method. The `pipeline` method accepts one argument: a closure that receives a Redis instance. You may issue all of your commands to this Redis instance and they will all be sent to the Redis server at the same time to reduce network trips to the server. The commands will still be executed in the order they were issued: - -```php -use Redis; -use Hypervel\Support\Facades; - -Facades\Redis::pipeline(function (Redis $pipe) { - for ($i = 0; $i < 1000; $i++) { - $pipe->set("key:$i", $i); - } -}); -``` - -## Pub / Sub - -Hypervel provides a convenient interface to the Redis `publish` and `subscribe` commands. These Redis commands allow you to listen for messages on a given "channel". You may publish messages to the channel from another application, or even using another programming language, allowing easy communication between applications and processes. - -First, let's setup a channel listener using the `subscribe` method. We'll place this method call within an [Artisan command](/docs/artisan) since calling the `subscribe` method begins a long-running process: - -```php - 'Adam Wathan' - ])); -}); -``` - -#### Wildcard Subscriptions - -Using the `psubscribe` method, you may subscribe to a wildcard channel, which may be useful for catching all messages on all channels. The channel name will be passed as the second argument to the provided closure: - -```php -Redis::psubscribe(['*'], function (string $message, string $channel) { - echo $message; -}); - -Redis::psubscribe(['users.*'], function (string $message, string $channel) { - echo $message; -}); \ No newline at end of file diff --git a/_archive/docs/0.3/docs/requests.md b/_archive/docs/0.3/docs/requests.md deleted file mode 100644 index 2ecd57ad87..0000000000 --- a/_archive/docs/0.3/docs/requests.md +++ /dev/null @@ -1,564 +0,0 @@ -# Requests -[[toc]] - -## Introduction - -Hypervel's `Hypervel\Http\Request` class provides an object-oriented way to interact with the current HTTP request being handled by your application as well as retrieve the input, cookies, and files that were submitted with the request. - -## Interacting With The Request - -### Accessing the Request - -To obtain an instance of the current HTTP request via dependency injection, you should type-hint the `Hypervel\Http\Request` class on your route closure or controller method. The incoming request instance will automatically be injected by the Hypervel [service container](/docs/container): - -```php -input('name'); - - // Store the user... - - return ['success' => true]; - } -} -``` - -As mentioned, you may also type-hint the `Hypervel\Http\Request` class on a route closure. The service container will automatically inject the incoming request into the closure when it is executed: - -```php -use Hypervel\Http\Request; - -Route::get('/', function (Request $request) { - // ... -}); -``` - -::: info -Remember everything injected by container is singleton? Then injecting `Hypervel\Http\Request` is safe here? Yes,it is. Request itself is a proxy class which fetches the real request data from context. -::: - -#### Dependency Injection and Route Parameters - -If your controller method is also expecting input from a route parameter you should list your route parameters after your other dependencies. For example, if your route is defined like so: - -```php -use App\Http\Controllers\UserController; - -Route::put('/user/{id}', [UserController::class, 'update']); -``` - -You may still type-hint the `Hypervel\Http\Request` and access your `id` route parameter by defining your controller method as follows: - -```php - true]; - } -} -``` - -### Request Path, Host, and Method - -The `Hypervel\Http\Request` instance provides a variety of methods for examining the incoming HTTP request. We will discuss a few of the most important methods below. - -::: warning -Unlike `Illuminate\Http\Request` in Laravel, `Hypervel\Http\Request` doesn't extend `Symfony\Component\HttpFoundation\Request`. Some methods available in Laravel's Request object might not be present in Hypervel's implementation. -::: - -#### Retrieving the Request Path - -The `path` method returns the request's path information. So, if the incoming request is targeted at `http://example.com/foo/bar`, the `path` method will return `foo/bar`: - -```php -$uri = $request->path(); -``` - -#### Inspecting the Request Path / Route - -The `is` method allows you to verify that the incoming request path matches a given pattern. You may use the `*` character as a wildcard when utilizing this method: - -```php -if ($request->is('admin/*')) { - // ... -} -``` - -#### Retrieving the Request URL - -To retrieve the full URL for the incoming request you may use the `url` or `fullUrl` methods. The `url` method will return the URL without the query string, while the `fullUrl` method includes the query string: - -```php -$url = $request->url(); - -$urlWithQueryString = $request->fullUrl(); -``` - -If you would like to append query string data to the current URL, you may call the `fullUrlWithQuery` method. This method merges the given array of query string variables with the current query string: - -```php -$request->fullUrlWithQuery(['type' => 'phone']); -``` - -If you would like to get the current URL without a given query string parameter, you may utilize the `fullUrlWithoutQuery` method: - -```php -$request->fullUrlWithoutQuery(['type']); -``` - -#### Retrieving the Request Host - -You may retrieve the "host" of the incoming request via the `host`, `httpHost`, and `schemeAndHttpHost` methods: - -```php -$request->host(); -$request->httpHost(); -$request->schemeAndHttpHost(); -``` - -#### Retrieving the Request Method - -The `method` method will return the HTTP verb for the request. You may use the `isMethod` method to verify that the HTTP verb matches a given string: - -```php -$method = $request->method(); - -if ($request->isMethod('post')) { - // ... -} -``` - -### Request Headers - -You may retrieve a request header from the `Hypervel\Http\Request` instance using the `header` method. If the header is not present on the request, `null` will be returned. However, the `header` method accepts an optional second argument that will be returned if the header is not present on the request: - -```php -$value = $request->header('X-Header-Name'); - -$value = $request->header('X-Header-Name', 'default'); -``` - -The `hasHeader` method may be used to determine if the request contains a given header: - -```php -if ($request->hasHeader('X-Header-Name')) { - // ... -} -``` - -For convenience, the `bearerToken` method may be used to retrieve a bearer token from the `Authorization` header. If no such header is present, an empty string will be returned: - -```php -$token = $request->bearerToken(); -``` - -### Content Negotiation - -Hypervel provides several methods for inspecting the incoming request's requested content types via the `Accept` header. First, the `getAcceptableContentTypes` method will return an array containing all of the content types accepted by the request: - -```php -$contentTypes = $request->getAcceptableContentTypes(); -``` - -The `accepts` method accepts an array of content types and returns `true` if any of the content types are accepted by the request. Otherwise, `false` will be returned: - -```php -if ($request->accepts(['text/html', 'application/json'])) { - // ... -} -``` - -You may use the `prefers` method to determine which content type out of a given array of content types is most preferred by the request. If none of the provided content types are accepted by the request, `null` will be returned: - -```php -$preferred = $request->prefers(['text/html', 'application/json']); -``` - -Since many applications only serve HTML or JSON, you may use the `expectsJson` method to quickly determine if the incoming request expects a JSON response: - -```php -if ($request->expectsJson()) { - // ... -} -``` - -### PSR-7 Requests - -The [PSR-7 standard](https://www.php-fig.org/psr/psr-7/) specifies interfaces for HTTP messages, including requests and responses. Requests and responses in Hypervel adhere to the PSR-7 standard by default. - -When injecting a request object, if it is declared as the PSR-7 standard interface `Psr\Http\Message\ServerRequestInterface`, Hypervel will automatically convert it to an equivalent `Hypervel\Http\Request` object. This `Hypervel\Http\Request` object implements the `Hyperf\HttpServer\Contract\RequestInterface`. - -::: tip -You can use `Hypervel\Http\Request` for injection so that you can get the IDE's auto-completion reminder support for exclusive methods. -::: - -## Input - -### Retrieving Input - -#### Retrieving All Input Data - -You may retrieve all of the incoming request's input data as an `array` using the `all` method. This method may be used regardless of whether the incoming request is from an HTML form or is an XHR request: - -```php -$input = $request->all(); -``` - -Using the `collect` method, you may retrieve all of the incoming request's input data as a [collection](/docs/collections): - -```php -$input = $request->collect(); -``` - -The `collect` method also allows you to retrieve a subset of the incoming request's input as a collection: - -```php -$request->collect('users')->each(function (string $user) { - // ... -}); -``` - -#### Retrieving an Input Value - -Using a few simple methods, you may access all of the user input from your `Hypervel\Http\Request` instance without worrying about which HTTP verb was used for the request. Regardless of the HTTP verb, the `input` method may be used to retrieve user input: - -```php -$name = $request->input('name'); -``` - -You may pass a default value as the second argument to the `input` method. This value will be returned if the requested input value is not present on the request: - -```php -$name = $request->input('name', 'Sally'); -``` - -When working with forms that contain array inputs, use "dot" notation to access the arrays: - -```php -$name = $request->input('products.0.name'); - -$names = $request->input('products.*.name'); -``` - -#### Retrieving Input From the Query String - -While the `input` method retrieves values from the entire request payload (including the query string), the `query` method will only retrieve values from the query string: - -```php -$name = $request->query('name'); -``` - -If the requested query string value data is not present, the second argument to this method will be returned: - -```php -$name = $request->query('name', 'Helen'); -``` - -You may call the `query` method without any arguments in order to retrieve all of the query string values as an associative array: - -```php -$query = $request->query(); -``` - -#### Retrieving JSON Input Values - -When sending JSON requests to your application, you may access the JSON data via the `input` method as long as the `Content-Type` header of the request is properly set to `application/json`. You may even use "dot" syntax to retrieve values that are nested within JSON arrays / objects: - -```php -$name = $request->input('user.name'); -``` - -#### Retrieving Stringable Input Values - -Instead of retrieving the request's input data as a primitive `string`, you may use the `string` method to retrieve the request data as an instance of [`Hypervel\Support\Str`](/docs/helpers#fluent-strings): - -```php -$name = $request->string('name')->trim(); -``` - -#### Retrieving Boolean Input Values - -When dealing with HTML elements like checkboxes, your application may receive "truthy" values that are actually strings. For example, "true" or "on". For convenience, you may use the `boolean` method to retrieve these values as booleans. The `boolean` method returns `true` for 1, "1", true, "true", "on", and "yes". All other values will return `false`: - -```php -$archived = $request->boolean('archived'); -``` - -#### Retrieving Date Input Values - -For convenience, input values containing dates / times may be retrieved as Carbon instances using the `date` method. If the request does not contain an input value with the given name, `null` will be returned: - -```php -$birthday = $request->date('birthday'); -``` - -The second and third arguments accepted by the `date` method may be used to specify the date's format and timezone, respectively: - -```php -$elapsed = $request->date('elapsed', '!H:i', 'Europe/Madrid'); -``` - -If the input value is present but has an invalid format, an `Carbon\Exceptions\InvalidFormatException` will be thrown; therefore, it is recommended that you validate the input before invoking the `date` method. - -#### Retrieving Enum Input Values - -Input values that correspond to [PHP enums](https://www.php.net/manual/en/language.types.enumerations.php) may also be retrieved from the request. If the request does not contain an input value with the given name or the enum does not have a backing value that matches the input value, `null` will be returned. The `enum` method accepts the name of the input value and the enum class as its first and second arguments: - -```php -use App\Enums\Status; - -$status = $request->enum('status', Status::class); -``` - -#### Request Context via Dynamic Properties - -Unlike dynamic properties in Laravel's request, dynamic properties are used to store contexts. You can store and retrieve request contexts via dynamic properties like so: - -```php -$request->name = 'Foo'; - -$request->name; -``` - -::: important -You won't get `name`'s value via `$request->input('name')`. -::: - -#### Retrieving a Portion of the Input Data - -If you need to retrieve a subset of the input data, you may use the `only` and `except` methods. Both of these methods accept a single `array` or a dynamic list of arguments: - -```php -$input = $request->only(['username', 'password']); - -$input = $request->only('username', 'password'); - -$input = $request->except(['credit_card']); - -$input = $request->except('credit_card'); -``` - -::: warning -The `only` method returns all of the key / value pairs that you request; however, it will not return key / value pairs that are not present on the request. -::: - -### Input Presence - -You may use the `has` method to determine if a value is present on the request. The `has` method returns `true` if the value is present on the request: - -```php -if ($request->has('name')) { - // ... -} -``` - -When given an array, the `has` method will determine if all of the specified values are present: - -```php -if ($request->has(['name', 'email'])) { - // ... -} -``` - -The `hasAny` method returns `true` if any of the specified values are present: - -```php -if ($request->hasAny(['name', 'email'])) { - // ... -} -``` - -The `whenHas` method will execute the given closure if a value is present on the request: - -```php -$request->whenHas('name', function (string $input) { - // ... -}); -``` - -A second closure may be passed to the `whenHas` method that will be executed if the specified value is not present on the request: - -```php -$request->whenHas('name', function (string $input) { - // The "name" value is present... -}, function () { - // The "name" value is not present... -}); -``` - -If you would like to determine if a value is present on the request and is not an empty string, you may use the `filled` method: - -```php -if ($request->filled('name')) { - // ... -} -``` - -The `anyFilled` method returns `true` if any of the specified values is not an empty string: - -```php -if ($request->anyFilled(['name', 'email'])) { - // ... -} -``` - -The `whenFilled` method will execute the given closure if a value is present on the request and is not an empty string: - -```php -$request->whenFilled('name', function (string $input) { - // ... -}); -``` - -A second closure may be passed to the `whenFilled` method that will be executed if the specified value is not "filled": - -```php -$request->whenFilled('name', function (string $input) { - // The "name" value is filled... -}, function () { - // The "name" value is not filled... -}); -``` - -To determine if a given key is absent from the request, you may use the `missing` and `whenMissing` methods: - -```php -if ($request->missing('name')) { - // ... -} - -$request->whenMissing('name', function (array $input) { - // The "name" value is missing... -}, function () { - // The "name" value is present... -}); -``` - -### Merging Additional Input - -Sometimes you may need to manually merge additional input into the request's existing input data. To accomplish this, you may use the `merge` method. If a given input key already exists on the request, it will be overwritten by the data provided to the `merge` method: - -```php -$request->merge(['votes' => 0]); -``` - -The `mergeIfMissing` method may be used to merge input into the request if the corresponding keys do not already exist within the request's input data: - -```php -$request->mergeIfMissing(['votes' => 0]); -``` - -### Cookies - -#### Retrieving Cookies From Requests - -All cookies created by the Hypervel is plaintext instead of encrypted data like in Laravel. To retrieve a cookie value from the request, use the `cookie` method on an `Hypervel\Http\Request` instance: - -```php -$value = $request->cookie('name'); -``` - -## Input Trimming and Normalization - -By default, Hypervel includes the `App\Http\Middleware\TrimStrings` and `App\Http\Middleware\ConvertEmptyStringsToNull` middleware in your application's global middleware stack. These middleware are listed in the global middleware stack by the `App\Http\Kernel` class but disabled by default. These middleware will automatically trim all incoming string fields on the request, as well as convert any empty string fields to `null`. This allows you to not have to worry about these normalization concerns in your routes and controllers. - -#### Disabling Input Normalization - -If you would like to disable this behavior for all requests, you may remove the two middleware from your application's middleware stack by removing them from the `$middleware` property of your `App\Http\Kernel` class. - -If you would like to disable string trimming and empty string conversion for a subset of requests to your application, you may use the `skipWhen` method offered by both middleware. This method accepts a closure which should return `true` or `false` to indicate if input normalization should be skipped. Typically, the `skipWhen` method should be invoked in the `boot` method of your application's `AppServiceProvider`. - -```php -use App\Http\Middleware\ConvertEmptyStringsToNull; -use App\Http\Middleware\TrimStrings; -use Psr\Http\Message\ServerRequestInterface; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - TrimStrings::skipWhen(function (ServerRequestInterface $request) { - return ! str_starts_with($request->getUri()->getPath(), '/admin/'); - }); - - ConvertEmptyStringsToNull::skipWhen(function (ServerRequestInterface $request) { - // ... - }); -} -``` - -## Files - -### Retrieving Uploaded Files - -You may retrieve uploaded files from an `Hypervel\Http\Request` instance using the `file` method. The `file` method returns an instance of the `Hyperf\HttpMessage\Upload\UploadedFile` class, which extends the PHP `SplFileInfo` class and provides a variety of methods for interacting with the file: - -```php -$file = $request->file('photo'); -``` - -You may determine if a file is present on the request using the `hasFile` method: - -```php -if ($request->hasFile('photo')) { - // ... -} -``` - -#### Validating Successful Uploads - -In addition to checking if the file is present, you may verify that there were no problems uploading the file via the `isValid` method: - -```php -if ($request->file('photo')->isValid()) { - // ... -} -``` - -#### File Paths and Extensions - -The `UploadedFile` class also contains methods for accessing the file's fully-qualified path and its extension. The `extension` method will attempt to guess the file's extension based on its contents. This extension may be different from the extension that was supplied by the client: - -```php -$path = $request->file('photo')->getPath(); - -$extension = $request->file('photo')->getExtension(); -``` - -#### Other File Methods - -There are a variety of other methods available on `UploadedFile` instances. Check out the [API documentation for the class](https://github.com/hyperf/hyperf/blob/3.1/src/http-message/src/Upload/UploadedFile.php) for more information regarding these methods. - -### Storing Uploaded Files - -To store an uploaded file, you will typically use `moveTo` in the `UploadedFile` class. It can help you move an uploaded file to your local filesystem. - -The `moveTo` method accepts the path where the file should be stored to. And the path should contain a filename. - -```php -$path = $request->file('photo')->moveTo('/images/foo.jpg'); -``` diff --git a/_archive/docs/0.3/docs/responses.md b/_archive/docs/0.3/docs/responses.md deleted file mode 100644 index 17a8f785a7..0000000000 --- a/_archive/docs/0.3/docs/responses.md +++ /dev/null @@ -1,247 +0,0 @@ -# Responses -[[toc]] - -## Creating Responses - -#### Strings and Arrays - -All routes and controllers should return a response to be sent back to the user's browser. Hypervel provides several different ways to return responses. The most basic response is returning a string from a route or controller. The framework will automatically convert the string into a full HTTP response: - -```php -Route::get('/', function () { - return 'Hello World'; -}); -``` - -In addition to returning strings from your routes and controllers, you may also return arrays. The framework will automatically convert the array into a JSON response: - -```php -Route::get('/', function () { - return [1, 2, 3]; -}); -``` - -::: tip -Did you know you can also return [Eloquent collections](/docs/eloquent-collections) from your routes or controllers? They will automatically be converted to JSON. Give it a shot! -::: - -#### Response Objects - -Typically, you won't just be returning simple strings or arrays from your route actions. Instead, you will be returning full `Hypervel\Http\Response` instances or [views](/docs/views). - -Returning a full `Response` instance allows you to customize the response's HTTP status code and headers. A `Response` instance inherits from the `Hypervel\Http\Response` class, which provides a variety of methods for building HTTP responses: - -```php -Route::get('/home', function () { - return response('Hello World', 200) - ->withHeader('Content-Type', 'text/plain'); -}); -``` - -::: warning -Unlike `Illuminate\Http\Response` in Laravel, `Hypervel\Http\Response` doesn't extend `Symfony\Component\HttpFoundation\Response`. Some methods available in Laravel's Response object might not be present in Hyperf's implementation. -::: - -#### Eloquent Models and Collections - -You may also return [Eloquent ORM](/docs/eloquent) models and collections directly from your routes and controllers. When you do, Hypervel will automatically convert the models and collections to JSON responses while respecting the model's [hidden attributes](/docs/eloquent-serialization#hiding-attributes-from-json): - -```php -use App\Models\User; - -Route::get('/user/{user}', function (User $user) { - return $user; -}); -``` - -### Attaching Headers to Responses - -Keep in mind that most response methods are chainable, allowing for the fluent construction of response instances. For example, you may use the `header` method to add a series of headers to the response before sending it back to the user: - -```php -return response($content) - ->withHeader('Content-Type', $type) - ->withHeader('X-Header-One', 'Header Value') - ->withHeader('X-Header-Two', 'Header Value'); -``` - -### Attaching Cookies to Responses - -You may attach a cookie to an outgoing `Psr\Http\Message\ResponseInterface` instance using the `withCookie` method. You should pass the name, value, and the number of minutes the cookie should be considered valid to this method: - -```php -use Hypervel\Support\Facades\Cookie; - -return response('Hello World') - ->withCookie(Cookie::make('name', 'value', $minutes)); -); -``` - -The `withCookie` method also accepts a few more arguments which are used less frequently. Generally, these arguments have the same purpose and meaning as the arguments that would be given to PHP's native [setcookie](https://secure.php.net/manual/en/function.setcookie.php) method: - -```php -use Hypervel\Support\Facades\Cookie; - -return response('Hello World') - ->withCookie( - Cookie::make('name', 'value', $minutes, $path, $domain, $secure, $httpOnly) - ); -``` - -If you would like to ensure that a cookie is sent with the outgoing response but you do not yet have an instance of that response, you can use the `Cookie` facade to "queue" cookies for attachment to the response when it is sent. The `queue` method accepts the arguments needed to create a cookie instance. These cookies will be attached to the outgoing response before it is sent to the browser: - -```php -use Hypervel\Support\Facades\Cookie; - -Cookie::queue('name', 'value', $minutes); -``` - -::: note -You need to enable `Hypervel\Cookie\Middleware\AddQueuedCookiesToResponse::class` middleware in your `app/Http/Kernel.php` file before using this feature. - -#### Generating Cookie Instances - -If you would like to generate a `Hypervel\Cookie\Cookie` instance that can be attached to a response instance at a later time, you may use the global `cookie` helper. This cookie will not be sent back to the client unless it is attached to a response instance: - -```php -$cookie = cookie('name', 'value', $minutes); - -return response('Hello World')->withCookie($cookie); -``` - -#### Expiring Cookies Early - -You may use the `Cookie` facade's `expire` method to expire a cookie: - -```php -use Hypervel\Support\Facades\Cookie; - -Cookie::expire('name'); -``` - -## Redirects - -Redirect responses are instances of the `Psr\Http\Message\ResponseInterface` class, and contain the proper headers needed to redirect the user to another URL. There are several ways to generate a redirect response. The simplest method is to use the global `redirect` helper: - -```php -Route::get('/dashboard', function () { - return redirect('home/dashboard'); -}); -``` - -## Other Response Types - -The `response` helper may be used to generate other types of response instances. When the `response` helper is called without arguments, an implementation of the `Hypervel\Http\Contracts\ResponseContract` [contract](/docs/contracts) is returned. This contract provides several helpful methods for generating responses. - -### View Responses - -If you need control over the response's status and headers but also need to return a [view](/docs/views) as the response's content, you should use the `view` method: - -```php -return response() - ->view('hello', $data, 200) - ->withHeader('Content-Type', $type); -``` - -Of course, if you do not need to pass a custom HTTP status code or custom headers, you may use the global `view` helper function. - -### JSON Responses - -The `json` method will automatically set the `Content-Type` header to `application/json`, as well as convert the given array to JSON using the `json_encode` PHP function: - -```php -return response()->json([ - 'name' => 'Abigail', - 'state' => 'CA', -], 200, ['X-Header-One' => 'Header Value']); -``` - -### File Downloads - -The `download` method may be used to generate a response that forces the user's browser to download the file at the given path. The `download` method accepts a filename as the second argument to the method, which will determine the filename that is seen by the user downloading the file. Finally, you may pass an array of HTTP headers as the third argument to the method: - -```php -return response()->download($pathToFile); - -return response()->download($pathToFile, $name); -``` - -#### Streamed Downloads - -Sometimes you may wish to turn the string response of a given operation into a downloadable response without having to write the contents of the operation to disk. You may use the `streamDownload` method in this scenario. This method accepts a callback, filename, and an optional array of headers as its arguments: - -```php -use App\Services\GitHub; - -return response()->streamDownload(function () { - return GitHub::api('repo') - ->contents() - ->readme('laravel', 'laravel')['contents']; -}, 'laravel-readme.md'); -``` - -In large data processing, you should chunk your output to avoid memory overflow. In this case, you can use the `write` method to write the data to the response: - -```php -use Hypervel\Http\StreamOutput; - -return response()->streamDownload(function (StreamOutput $output) { - $response->write('large-data-chunk-1'); - // ... - $response->write('large-data-chunk-5'); -}, 'large-data.txt', $headers); -``` - -#### Server Side Events - -The `stream` method may be used to stream a response to the client. This method is useful for streaming large data or for implementing server-side events. The `stream` method accepts a callback as its first argument, which will be called repeatedly to stream the response to the client. The callback will receive the `StreamOutput` instance as its only argument: - -```php -use Hypervel\Http\StreamOutput; - -return response()->stream(function (StreamOutput $output) { - $output->write('Hello World'); - // ... - $output->write('Hypervel is awesome!'); -}, $headers); -``` - -### File Responses - -The `file` method may be used to display a file, such as an image or PDF, directly in the user's browser instead of initiating a download. This method accepts the absolute path to the file: - -```php -return response()->file($pathToFile); -``` - -## Response Macros - -If you would like to define a custom response that you can re-use in a variety of your routes and controllers, you may use the `macro` method on the `Response` facade. Typically, you should call this method from the `boot` method of one of your application's [service providers](/docs/providers), such as the `App\Providers\AppServiceProvider` service provider: - -```php -caps('foo'); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/routing.md b/_archive/docs/0.3/docs/routing.md deleted file mode 100644 index d6cb9201c3..0000000000 --- a/_archive/docs/0.3/docs/routing.md +++ /dev/null @@ -1,401 +0,0 @@ -# Routing -[[toc]] - -## Basic Routing - -The most basic Hypervel routes accept a URI and a closure, providing a very simple and expressive method of defining routes and behavior without complicated routing configuration files: - -```php -use Hypervel\Support\Facades\Route; - -Route::get('/greeting', function () { - return 'Hello World'; -}); -``` - -::: important -Hypervel uses `nikic/fast-route` as the core routing component. And the usage of Hypervel's routing system is similar to routing in Lumen framework. -::: - -#### The Default Route Files - -All Hypervel routes are defined in your route files, which are located in the `routes` directory. These files are automatically loaded by your application's `App\Providers\RouteServiceProvider`. The `routes/web.php` file defines routes that are for your web interface. These routes are assigned the `web` middleware group. The routes in `routes/api.php` are stateless and are assigned the `api` middleware group. - -For most applications, you will begin by defining routes in your `routes/web.php` file. The routes defined in `routes/web.php` may be accessed by entering the defined route's URL in your browser. For example, you may access the following route by navigating to `http://example.com/user` in your browser: - -```php -use App\Http\Controllers\UserController; - -Route::get('/user', [UserController::class, 'index']); -``` - -Routes defined in the `routes/api.php` file are nested within a route group by the `RouteServiceProvider`. Within this group, the `/api` URI prefix is automatically applied so you do not need to manually apply it to every route in the file. You may modify the prefix and other route group options by modifying your `RouteServiceProvider` class. - -#### Available Router Methods - -The router allows you to register routes that respond to any HTTP verb: - -```php -Route::get($uri, $callback, $options); -Route::post($uri, $callback, $options); -Route::put($uri, $callback, $options); -Route::patch($uri, $callback, $options); -Route::delete($uri, $callback, $options); -Route::options($uri, $callback, $options); -``` - -Sometimes you may need to register a route that responds to multiple HTTP verbs. You may do so using the `match` method. Or, you may even register a route that responds to all HTTP verbs using the `any` method: - -```php -Route::match(['get', 'post'], '/', function () { - // ... -}); - -Route::any('/', function () { - // ... -}); -``` - -::: important -When defining multiple routes that share the same URI, routes using the `get`, `post`, `put`, `patch`, `delete`, and `options` methods should be defined before routes using the `any` and `match` methods. This ensures the incoming request is matched with the correct route. -::: - -#### Dependency Injection - -You may type-hint any dependencies required by your route in your route's callback signature. The declared dependencies will automatically be resolved and injected into the callback by the Laravel [service container](/docs/container). For example, you may type-hint the `Hypervel\Http\Request` class to have the current HTTP request automatically injected into your route callback: - -```php -use Hypervel\Http\Request; - -Route::get('/users', function (Request $request) { - // ... -}); -``` - -### The Route List - -The `route:list` Artisan command can easily provide an overview of all of the routes that are defined by your application: - -```shell -php artisan route:list -``` - -You may also instruct Hypervel to only show routes that begin with a given URI: - -```shell -php artisan route:list --path=api -``` - -## Route Parameters - -### Required Parameters - -Sometimes you will need to capture segments of the URI within your route. For example, you may need to capture a user's ID from the URL. You may do so by defining route parameters: - -```php -Route::get('/user/{id}', function (string $id) { - return 'User '.$id; -}); -``` - -You may define as many route parameters as required by your route: - -```php -Route::get('/posts/{post}/comments/{comment}', function (string $postId, string $commentId) { - // ... -}); -``` - -Route parameters are always encased within "curly" braces. The parameters will be passed into your route's Closure when the route is executed. - -::: warning -Route parameters cannot contain the `-` character. Use an underscore (`_`) instead. -::: - -#### Parameters and Dependency Injection - -If your route has dependencies that you would like the Hypervel service container to automatically inject into your route's callback, you should list your route parameters after your dependencies: - -```php -use Hypervel\Http\Request; - -Route::get('/user/{id}', function (Request $request, string $id) { - return 'User '.$id; -}); -``` - -### Optional Parameters - -You may define optional route parameters by enclosing part of the route URI definition in `[...]`. So, for example, `/foo[bar]` will match both `/foo` and `/foobar`. Optional parameters are only supported in a trailing position of the URI. In other words, you may not place an optional parameter in the middle of a route definition: - -```php -Route::get('user[/{name}]', function ($name = null) { - return $name; -}); -``` - -### Regular Expression Constraints - -You may constrain the format of your route parameters using the `where` method on a route instance. The `where` method accepts the name of the parameter and a regular expression defining how the parameter should be constrained: - -```php -Route::get('user/{name:[A-Za-z]+}', function ($name) { - // -}); -``` - -## Named Routes - -Named routes allow the convenient generation of URLs or redirects for specific routes. You may specify a name for a route using the as array key when defining the route: - -```php -Route::get('profile', ['as' => 'profile', function () { - // -}]); -``` - -You may also specify route names for controller actions: - -```php -Route::get('profile', [ - 'as' => 'profile', 'uses' => 'UserController@showProfile' -]); -``` - -::: important -Route names should always be unique. -::: - -#### Generating URLs to Named Routes - -Once you have assigned a name to a given route, you may use the route's name when generating URLs or redirects via Laravel's `route` and `redirect` helper functions: - -```php -// Generating URLs... -$url = route('profile'); -``` - -If the named route defines parameters, you may pass the parameters as the second argument to the `route` function. The given parameters will automatically be inserted into the URL in their correct positions: - -```php -Route::get('user/{id}/profile', ['as' => 'profile', function ($id) { - // -}]); - -$url = route('profile', ['id' => 1]); -``` - -## Route Groups - -Route groups allow you to share route attributes, such as middleware or namespaces, across a large number of routes without needing to define those attributes on each individual route. Shared attributes are specified in an array format as the first parameter to the `Route::group` method. - -To learn more about route groups, we'll walk through several common use-cases for the feature. - -### Middleware - -To assign [middleware](/docs/middleware) to all routes within a group, you may use the `middleware` method before defining the group. Middleware are executed in the order they are listed in the array: - -```php -Route::group('api', function () { - Route::get('/', function () { - // Uses Auth Middleware - }); - - Route::get('user/profile', function () { - // Uses Auth Middleware - }); -}, ['middleware' => 'auth']); -``` - -### Namespaces - -Another common use-case for route groups is assigning the same PHP namespace to a group of controllers. You may use the `namespace` parameter in your group attribute array to specify the namespace for all controllers within the group: - -```php -Route::group('/api', function () { - Route::get('/', function () { - // Using The "App\Http\Controllers\Admin" Namespace... - }); -}, ['namespace' => 'App\Http\Controllers\Admin']); -``` - -### Route Prefixes - -The route in the group definition will serve as prefix for nested routes with a given URI. For example, you may want to prefix all route URIs within the group with `admin`: - -```php -Route::group('/admin', function () { - Route::get('/users', function () { - // Matches The "/admin/users" URL - }); -}); -``` - -You can also add `prefix` option to the routes: - -```php -Route::get('users', function () { - // Matches The "/api/users" URL -}, ['prefix' => 'api']); - -Route::group('/admin', function () { - Route::get('/users', function () { - // Matches The "/api/admin/users" URL - }); -}, ['prefix' => 'api']); -``` - -## Route Model Binding - -When injecting a model ID to a route or controller action, you will often query the database to retrieve the model that corresponds to that ID. Hypervel route model binding provides a convenient way to automatically inject the model instances directly into your routes. For example, instead of injecting a user's ID, you can inject the entire `User` model instance that matches the given ID. - -### Implicit Binding - -Hypervel automatically resolves Eloquent models defined in routes or controller actions whose type-hinted variable names match a route segment name. For example: - -```php -use App\Models\User; - -Route::get('/users/{user}', function (User $user) { - return $user->email; -}); -``` - -Since the `$user` variable is type-hinted as the `App\Models\User` Eloquent model and the variable name matches the `{user}` URI segment, Hypervel will automatically inject the model instance that has an ID matching the corresponding value from the request URI. If a matching model instance is not found in the database, a 404 HTTP response will automatically be generated. - -Of course, implicit binding is also possible when using controller methods. Again, note the `{user}` URI segment matches the `$user` variable in the controller which contains an `App\Models\User` type-hint: - -```php -use App\Http\Controllers\UserController; -use App\Models\User; - -// Route definition... -Route::get('/users/{user}', [UserController::class, 'show']); - -// Controller method definition... -public function show(User $user) -{ - return view('user.profile', ['user' => $user]); -} -``` - -::: info -This feature is provided via `Hypervel\Router\Middleware\SubstituteBindings`, and it's disabled by default. You can extend this middleware to customize your resolving behaviors. -::: - -### Implicit Enum Binding - -PHP 8.1 introduced support for [Enums](https://www.php.net/manual/en/language.enumerations.backed.php). To complement this feature, Laravel allows you to type-hint a [string-backed Enum](https://www.php.net/manual/en/language.enumerations.backed.php) on your route definition and Laravel will only invoke the route if that route segment corresponds to a valid Enum value. Otherwise, a 404 HTTP response will be returned automatically. For example, given the following Enum: - -```php -value; -}); -``` - -### Explicit Binding - -You are not required to use Hypervel's implicit, convention based model resolution in order to use model binding. You can also explicitly define how route parameters correspond to models. To register an explicit binding, use the router's `model` method to specify the class for a given parameter. You should define your explicit model bindings at the beginning of the `boot` method of your `AppServiceProvider` class: - -```php -use App\Models\User; -use Hypervel\Support\Facades\Route; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Route::model('user', User::class); -} -``` - -Next, define a route that contains a `{user}` parameter: - -```php -use App\Models\User; - -Route::get('/users/{user}', function (User $user) { - // ... -}); -``` - -Since we have bound all `{user}` parameters to the `App\Models\User` model, an instance of that class will be injected into the route. So, for example, a request to `users/1` will inject the `User` instance from the database which has an ID of `1`. - -If a matching model instance is not found in the database, a 404 HTTP response will be automatically generated. - -#### Customizing the Resolution Logic - -If you wish to define your own model binding resolution logic, you may use the `Route::bind` method. The closure you pass to the `bind` method will receive the value of the URI segment and should return the instance of the class that should be injected into the route. Again, this customization should take place in the `boot` method of your application's `AppServiceProvider`: - -```php -use App\Models\User; -use Hypervel\Support\Facades\Route; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Route::bind('user', function (string $value) { - return User::where('name', $value)->firstOrFail(); - }); -} -``` - -Alternatively, you may override the `resolveRouteBinding` method on your Eloquent model. This method will receive the value of the URI segment and should return the instance of the class that should be injected into the route: - -```php -/** - * Retrieve the model for a bound value. - * - * @param mixed $value - * @param string|null $field - * @return \Hypervel\Database\Eloquent\Model|null - */ -public function resolveRouteBinding($value, $field = null) -{ - return $this->where('name', $value)->firstOrFail(); -} -``` - -## Throttling With Redis - -Typically, the `throttle` middleware is mapped to the `Hypervel\Router\Middleware\ThrottleRequests` class. This mapping is defined in your application's HTTP kernel (`App\Http\Kernel`). - -```php -'throttle' => \Hypervel\Router\Middleware\ThrottleRequests::class, -``` - -## Cross-Origin Resource Sharing (CORS) - -Hypervel can automatically respond to CORS `OPTIONS` HTTP requests with values that you configure. The `OPTIONS` requests will automatically be handled by the `HandleCors` [middleware]((/docs/middleware)) that is automatically included in your application's global middleware stack. - -Sometimes, you may need to customize the CORS configuration values for your application. You may do so by publishing the `cors` configuration file using the `config:publish` Artisan command: - -```shell -php artisan vendor:publish hypervel/http -``` - -This command will place a `cors.php` configuration file within your application's `config` directory. - -::: note -For more information on CORS and CORS headers, please consult the [MDN web documentation on CORS.](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#The_HTTP_response_headers) -::: \ No newline at end of file diff --git a/_archive/docs/0.3/docs/sanctum.md b/_archive/docs/0.3/docs/sanctum.md deleted file mode 100644 index 8a0f9eede8..0000000000 --- a/_archive/docs/0.3/docs/sanctum.md +++ /dev/null @@ -1,490 +0,0 @@ -# Hypervel Sanctum -[[toc]] - -## Introduction - -[Hypervel Sanctum](https://github.com/hypervel/sanctum) provides a featherweight authentication system for SPAs (single page applications), mobile applications, and simple, token based APIs. Sanctum allows each user of your application to generate multiple API tokens for their account. These tokens may be granted abilities / scopes which specify which actions the tokens are allowed to perform. - -### How it Works - -Hypervel Sanctum exists to solve two separate problems. Let's discuss each before digging deeper into the library. - - -#### API Tokens - -First, Sanctum is a simple package you may use to issue API tokens to your users without the complication of OAuth. This feature is inspired by GitHub and other applications which issue "personal access tokens". For example, imagine the "account settings" of your application has a screen where a user may generate an API token for their account. You may use Sanctum to generate and manage those tokens. These tokens typically have a very long expiration time (years), but may be manually revoked by the user anytime. - -Hypervel Sanctum offers this feature by storing user API tokens in a single database table and authenticating incoming HTTP requests via the `Authorization` header which should contain a valid API token. - - -#### SPA Authentication - -Second, Sanctum exists to offer a simple way to authenticate single page applications (SPAs) that need to communicate with a Hypervel powered API. These SPAs might exist in the same repository as your Hypervel application or might be an entirely separate repository, such as an SPA created using Next.js or Nuxt. - -For this feature, Sanctum does not use tokens of any kind. Instead, Sanctum uses Hypervel's built-in cookie based session authentication services. Typically, Sanctum utilizes Hypervel's `web` authentication guard to accomplish this. This provides the benefits of CSRF protection, session authentication, as well as protects against leakage of the authentication credentials via XSS. - -Sanctum will only attempt to authenticate using cookies when the incoming request originates from your own SPA frontend. When Sanctum examines an incoming HTTP request, it will first check for an authentication cookie and, if none is present, Sanctum will then examine the `Authorization` header for a valid API token. - -::: note -It is perfectly fine to use Sanctum only for API token authentication or only for SPA authentication. Just because you use Sanctum does not mean you are required to use both features it offers. -::: - -## Installation - -You may install Hypervel Sanctum via the Composer package manager: - -```shell -composer require hypervel/sanctum -``` - -Next, you should publish the Sanctum configuration and migration files using the `vendor:publish` Artisan command. The sanctum configuration file will be placed in your application's `config` directory: - -```shell -php artisan vendor:publish "Hypervel\Sanctum\SanctumServiceProvider" -``` - -Finally, you should run your database migrations. Sanctum will create one database table in which to store API tokens: - -```shell -php artisan migrate -``` - -Next, if you plan to utilize Sanctum to authenticate a SPA, you should add Sanctum's middleware to your `api` middleware group within your application's `app/Http/Kernel.php` file: - -```php -'api' => [ - \Hypervel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, - \Hypervel\Router\Middleware\ThrottleRequests::class . '60,1,api', - \Hypervel\Router\Middleware\SubstituteBindings::class, -], -``` - -## Configuration - -### Setting Sanctum Guard - -Before authenticating with sanctum, you need to configure `sanctum` guard in `config/auth.php`. - -```php -'guards' => [ - // ... - 'sanctum' => [ - 'driver' => 'sanctum', - 'provider' => 'users', - ], -], -``` - -### Overriding Default Models - -Although not typically required, you are free to extend the `PersonalAccessToken` model used internally by Sanctum: - -```php -use Hypervel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken; - -class PersonalAccessToken extends SanctumPersonalAccessToken -{ - // ... -} -``` - -Then, you may instruct Sanctum to use your custom model via the `usePersonalAccessTokenModel` method provided by Sanctum. Typically, you should call this method in the `boot` method of your application's `AppServiceProvider` file: - -```php -use App\Models\Sanctum\PersonalAccessToken; -use Hypervel\Sanctum\Sanctum; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); -} -``` - -## API Token Authentication - -::: note -You should not use API tokens to authenticate your own first-party SPA. Instead, use Sanctum's built-in [SPA authentication features](#spa-authentication). -::: - -### Issuing API Tokens - -Sanctum allows you to issue API tokens / personal access tokens that may be used to authenticate API requests to your application. When making requests using API tokens, the token should be included in the `Authorization` header as a `Bearer` token. - -To begin issuing tokens for users, your User model should use the `Hypervel\Sanctum\HasApiTokens` trait: - -```php -use Hypervel\Sanctum\HasApiTokens; - -class User extends Authenticatable -{ - use HasApiTokens, HasFactory, Notifiable; -} -``` - -To issue a token, you may use the `createToken` method. The `createToken` method returns a `Hypervel\Sanctum\NewAccessToken` instance. API tokens are hashed using SHA-256 hashing before being stored in your database, but you may access the plain-text value of the token using the `plainTextToken` property of the `NewAccessToken` instance. You should display this value to the user immediately after the token has been created: - -```php -use Hypervel\Http\Request; - -Route::post('/tokens/create', function (Request $request) { - $token = $request->user()->createToken($request->token_name); - - return ['token' => $token->plainTextToken]; -}); -``` - -You may access all of the user's tokens using the `tokens` Eloquent relationship provided by the `HasApiTokens` trait: - -```php -foreach ($user->tokens as $token) { - // ... -} -``` - -### Token Abilities - -Sanctum allows you to assign "abilities" to tokens. Abilities serve a similar purpose as OAuth's "scopes". You may pass an array of string abilities as the second argument to the `createToken` method: - -```php -return $user->createToken('token-name', ['server:update'])->plainTextToken; -``` - -When handling an incoming request authenticated by Sanctum, you may determine if the token has a given ability using the `tokenCan` or `tokenCant` methods: - -```php -if ($user->tokenCan('server:update')) { - // ... -} - -if ($user->tokenCant('server:update')) { - // ... -} -``` - -#### Token Ability Middleware - -Sanctum also includes two middleware that may be used to verify that an incoming request is authenticated with a token that has been granted a given ability. To get started, define the following middleware aliases in your application's `app/Http/Kernel.php` file: - -```php -use Hypervel\Sanctum\Http\Middleware\CheckAbilities; -use Hypervel\Sanctum\Http\Middleware\CheckForAnyAbility; - -protected array $middlewareAliases = [ - 'abilities' => CheckAbilities::class, - 'ability' => CheckForAnyAbility::class, -]; -``` - -The `abilities` middleware may be assigned to a route to verify that the incoming request's token has all of the listed abilities: - -```php -Route::get('/orders', function () { - // Token has both "check-status" and "place-orders" abilities... -}, [ - 'middleware' => ['auth:sanctum', 'abilities:check-status,place-orders'] -]); -``` - -The `ability` middleware may be assigned to a route to verify that the incoming request's token has *at least one* of the listed abilities: - -```php -Route::get('/orders', function () { - // Token has the "check-status" or "place-orders" ability... -}, [ - 'middleware' => ['auth:sanctum', 'ability:check-status,place-orders'] -]); -``` - -#### First-Party UI Initiated Requests - -For convenience, the `tokenCan` method will always return `true` if the incoming authenticated request was from your first-party SPA and you are using Sanctum's built-in [SPA authentication](#spa-authentication). - -However, this does not necessarily mean that your application has to allow the user to perform the action. Typically, your application's [authorization policies](/docs/authorization#creating-policies) will determine if the token has been granted the permission to perform the abilities as well as check that the user instance itself should be allowed to perform the action. - -For example, if we imagine an application that manages servers, this might mean checking that the token is authorized to update servers **and** that the server belongs to the user: - -```php -return $request->user()->id === $server->user_id && - $request->user()->tokenCan('server:update') -``` - -At first, allowing the `tokenCan` method to be called and always return `true` for first-party UI initiated requests may seem strange; however, it is convenient to be able to always assume an API token is available and can be inspected via the `tokenCan` method. By taking this approach, you may always call the `tokenCan` method within your application's authorization policies without worrying about whether the request was triggered from your application's UI or was initiated by one of your API's third-party consumers. - -### Protecting Routes - -To protect routes so that all incoming requests must be authenticated, you should attach the `sanctum` authentication guard to your protected routes within your `routes/web.php` and `routes/api.php` route files. This guard will ensure that incoming requests are authenticated as either stateful, cookie authenticated requests or contain a valid API token header if the request is from a third party. - -You may be wondering why we suggest that you authenticate the routes within your application's `routes/web.php` file using the `sanctum` guard. Remember, Sanctum will first attempt to authenticate incoming requests using Hypervel's typical session authentication cookie. If that cookie is not present then Sanctum will attempt to authenticate the request using a token in the request's `Authorization` header. In addition, authenticating all requests using Sanctum ensures that we may always call the `tokenCan` method on the currently authenticated user instance: - -```php -use Hypervel\Http\Request; - -Route::get('/user', function (Request $request) { - return $request->user(); -}, ['middleware' => 'auth:sanctum']); -``` - -### Revoking Tokens - -You may "revoke" tokens by deleting them from your database using the `tokens` relationship that is provided by the `Hypervel\Sanctum\HasApiTokens` trait: - -```php -// Revoke all tokens... -$user->tokens()->delete(); - -// Revoke the token that was used to authenticate the current request... -$request->user()->currentAccessToken()->delete(); - -// Revoke a specific token... -$user->tokens()->where('id', $tokenId)->delete(); -``` - -### Token Expiration - -By default, Sanctum tokens never expire and may only be invalidated by [revoking the token](#revoking-tokens). However, if you would like to configure an expiration time for your application's API tokens, you may do so via the `expiration` configuration option defined in your application's `sanctum` configuration file. This configuration option defines the number of minutes until an issued token will be considered expired: - -```php -'expiration' => 525600, -``` - -If you would like to specify the expiration time of each token independently, you may do so by providing the expiration time as the third argument to the `createToken` method: - -```php -return $user->createToken( - 'token-name', ['*'], now()->addWeek() -)->plainTextToken; -``` - -If you have configured a token expiration time for your application, you may also wish to [schedule a task](/docs/scheduling) to prune your application's expired tokens. Thankfully, Sanctum includes a `sanctum:prune-expired` Artisan command that you may use to accomplish this. For example, you may configure a scheduled task to delete all expired token database records that have been expired for at least 24 hours: - -```php -use Hypervel\Support\Facades\Schedule; - -Schedule::command('sanctum:prune-expired --hours=24')->daily(); -``` - -## SPA Authentication - -Sanctum also exists to provide a simple method of authenticating single page applications (SPAs) that need to communicate with a Hypervel powered API. These SPAs might exist in the same repository as your Hypervel application or might be an entirely separate repository. - -For this feature, Sanctum does not use tokens of any kind. Instead, Sanctum uses Hypervel's built-in cookie based session authentication services. This approach to authentication provides the benefits of CSRF protection, session authentication, as well as protects against leakage of the authentication credentials via XSS. - -::: warning -In order to authenticate, your SPA and API must share the same top-level domain. However, they may be placed on different subdomains. Additionally, you should ensure that you send the `Accept: application/json` header and either the `Referer` or `Origin` header with your request. -::: - -### Configuration - -#### Configuring Your First-Party Domains - -First, you should configure which domains your SPA will be making requests from. You may configure these domains using the `stateful` configuration option in your `sanctum` configuration file. This configuration setting determines which domains will maintain "stateful" authentication using Hypervel session cookies when making requests to your API. - -To assist you in setting up your first-party stateful domains, Sanctum provides two helper functions that you can include in the configuration. First, `Sanctum::currentApplicationUrlWithPort()` will return the current application URL from the `APP_URL` environment variable, and `Sanctum::currentRequestHost()` will inject a placeholder into the stateful domain list which, at runtime, will be replaced by the host from the current request so that all requests with the same domain are considered stateful. - -::: warning -If you are accessing your application via a URL that includes a port (`127.0.0.1:8000`), you should ensure that you include the port number with the domain. -::: - -#### Sanctum Middleware - -Next, you should add Sanctum's middleware to your api middleware group within your `app/Http/Kernel.php` file. This middleware is responsible for ensuring that incoming requests from your SPA can authenticate using Hypervel's session cookies, while still allowing requests from third parties or mobile applications to authenticate using API tokens: - -```php -'api' => [ - \Hypervel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, - \Hypervel\Router\Middleware\ThrottleRequests::class . ':60,1,api', - \Hypervel\Router\Middleware\SubstituteBindings::class, -], -``` - -#### CORS and Cookies - -If you are having trouble authenticating with your application from an SPA that executes on a separate subdomain, you have likely misconfigured your CORS (Cross-Origin Resource Sharing) or session cookie settings. - -Next, you should ensure that your application's CORS configuration is returning the `Access-Control-Allow-Credentials` header with a value of `True`. This may be accomplished by setting the `supports_credentials` option within your application's `config/cors.php` configuration file to `true`. - -In addition, you should enable the `withCredentials` and `withXSRFToken` options on your application's global `axios` instance. Typically, this should be performed in your `resources/js/bootstrap.js` file. If you are not using Axios to make HTTP requests from your frontend, you should perform the equivalent configuration on your own HTTP client: - -```js -axios.defaults.withCredentials = true; -axios.defaults.withXSRFToken = true; -``` - -Finally, you should ensure your application's session cookie domain configuration supports any subdomain of your root domain. You may accomplish this by prefixing the domain with a leading `.` within your application's `config/session.php` configuration file: - -```php -'domain' => '.domain.com', -``` - - -### Authenticating - -#### CSRF Protection - -To authenticate your SPA, your SPA's "login" page should first make a request to the `/sanctum/csrf-cookie` endpoint to initialize CSRF protection for the application: - -```js -axios.get('/sanctum/csrf-cookie').then(response => { - // Login... -}); -``` - -During this request, Hypervel will set an `XSRF-TOKEN` cookie containing the current CSRF token. This token should then be URL decoded and passed in an `X-XSRF-TOKEN` header on subsequent requests, which some HTTP client libraries like Axios and the Angular HttpClient will do automatically for you. If your JavaScript HTTP library does not set the value for you, you will need to manually set the `X-XSRF-TOKEN` header to match the URL decoded value of the `XSRF-TOKEN` cookie that is set by this route. - -#### Logging In - -Once CSRF protection has been initialized, you should make a `POST` request to your Hypervel application's `/login` route. This `/login` route may be [implemented manually](/docs/authentication#manually-authenticating-users). - -If the login request is successful, you will be authenticated and subsequent requests to your application's routes will automatically be authenticated via the session cookie that the Hypervel application issued to your client. In addition, since your application already made a request to the `/sanctum/csrf-cookie` route, subsequent requests should automatically receive CSRF protection as long as your JavaScript HTTP client sends the value of the `XSRF-TOKEN` cookie in the `X-XSRF-TOKEN` header. - -Of course, if your user's session expires due to lack of activity, subsequent requests to the Hypervel application may receive a 401 or 419 HTTP error response. In this case, you should redirect the user to your SPA's login page. - -::: warning -You are free to write your own `/login` endpoint; however, you should ensure that it authenticates the user using the standard, [session based authentication services that Hypervel provides](/docs/authentication#authenticating-users). Typically, this means using the `web` authentication guard. -::: - - -### Protecting Routes - -To protect routes so that all incoming requests must be authenticated, you should attach the `sanctum` authentication guard to your API routes within your `routes/api.php` file. This guard will ensure that incoming requests are authenticated as either stateful authenticated requests from your SPA or contain a valid API token header if the request is from a third party: - -```php -use Hypervel\Http\Request; - -Route::get('/user', function (Request $request) { - return $request->user(); -}, ['middleware' => 'auth:sanctum']); -``` - -### Authorizing Private Broadcast Channels - -If your SPA needs to authenticate with [private / presence broadcast channels](/docs/broadcasting#authorizing-channels), you should place the `Broadcast::routes` method call within your `routes/api.php` file: - -```php -Broadcast::routes(['middleware' => ['auth:sanctum']]); -``` - -Next, in order for Pusher's authorization requests to succeed, you will need to provide a custom Pusher `authorizer` when initializing [Laravel Echo](/docs/broadcasting#client-side-installation). This allows your application to configure Pusher to use the `axios` instance that is [properly configured for cross-domain requests](#cors-and-cookies): - -```js -window.Echo = new Echo({ - broadcaster: "pusher", - cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER, - encrypted: true, - key: import.meta.env.VITE_PUSHER_APP_KEY, - authorizer: (channel, options) => { - return { - authorize: (socketId, callback) => { - axios.post('/api/broadcasting/auth', { - socket_id: socketId, - channel_name: channel.name - }) - .then(response => { - callback(false, response.data); - }) - .catch(error => { - callback(true, error); - }); - } - }; - }, -}) -``` - -## Mobile Application Authentication - -You may also use Sanctum tokens to authenticate your mobile application's requests to your API. The process for authenticating mobile application requests is similar to authenticating third-party API requests; however, there are small differences in how you will issue the API tokens. - - -### Issuing API Tokens - -To get started, create a route that accepts the user's email / username, password, and device name, then exchanges those credentials for a new Sanctum token. The "device name" given to this endpoint is for informational purposes and may be any value you wish. In general, the device name value should be a name the user would recognize, such as "Nuno's iPhone 12". - -Typically, you will make a request to the token endpoint from your mobile application's "login" screen. The endpoint will return the plain-text API token which may then be stored on the mobile device and used to make additional API requests: - -```php -use App\Models\User; -use Hypervel\Http\Request; -use Hypervel\Support\Facades\Hash; -use Hypervel\Validation\ValidationException; - -Route::post('/sanctum/token', function (Request $request) { - $request->validate([ - 'email' => 'required|email', - 'password' => 'required', - 'device_name' => 'required', - ]); - - $user = User::where('email', $request->email)->first(); - - if (! $user || ! Hash::check($request->password, $user->password)) { - throw ValidationException::withMessages([ - 'email' => ['The provided credentials are incorrect.'], - ]); - } - - return $user->createToken($request->device_name)->plainTextToken; -}); -``` - -When the mobile application uses the token to make an API request to your application, it should pass the token in the `Authorization` header as a `Bearer` token. - -::: note -When issuing tokens for a mobile application, you are also free to specify [token abilities](#token-abilities). -::: - - -### Protecting Routes - -As previously documented, you may protect routes so that all incoming requests must be authenticated by attaching the `sanctum` authentication guard to the routes: - -```php -Route::get('/user', function (Request $request) { - return $request->user(); -}, ['middleware' => 'auth:sanctum']); -``` - -### Revoking Tokens - -To allow users to revoke API tokens issued to mobile devices, you may list them by name, along with a "Revoke" button, within an "account settings" portion of your web application's UI. When the user clicks the "Revoke" button, you can delete the token from the database. Remember, you can access a user's API tokens via the `tokens` relationship provided by the `Hypervel\Sanctum\HasApiTokens` trait: - -```php -// Revoke all tokens... -$user->tokens()->delete(); - -// Revoke a specific token... -$user->tokens()->where('id', $tokenId)->delete(); -``` - -## Testing - -While testing, the `Sanctum::actingAs` method may be used to authenticate a user and specify which abilities should be granted to their token: - -```php -use App\Models\User; -use Hypervel\Sanctum\Sanctum; - -public function testTaskListCanBeRetrieved(): void -{ - Sanctum::actingAs( - User::factory()->create(), - ['view-tasks'] - ); - - $response = $this->get('/api/task'); - - $response->assertOk(); -} -``` - -If you would like to grant all abilities to the token, you should include `*` in the ability list provided to the `actingAs` method: - -```php -Sanctum::actingAs( - User::factory()->create(), - ['*'] -); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/scheduling.md b/_archive/docs/0.3/docs/scheduling.md deleted file mode 100644 index 68b07252ee..0000000000 --- a/_archive/docs/0.3/docs/scheduling.md +++ /dev/null @@ -1,554 +0,0 @@ -# Task Scheduling -[[toc]] - -## Introduction - -In the past, you may have written a cron configuration entry for each task you needed to schedule on your server. However, this can quickly become a pain because your task schedule is no longer in source control and you must SSH into your server to view your existing cron entries or add additional entries. - -Hypervel's command scheduler offers a fresh approach to managing scheduled tasks on your server. The scheduler allows you to fluently and expressively define your command schedule within your Hypervel application itself. When using the scheduler, only a single cron entry is needed on your server. Your task schedule is defined in the `app/Console/Kernel.php` file's `schedule` method. To help you get started, a simple example is defined within the method. - -## Defining Schedules - -You may define all of your scheduled tasks in the `schedule` method of your application's `App\Console\Kernel` class. To get started, let's take a look at an example. In this example, we will schedule a closure to be called every day at midnight. Within the closure we will execute a database query to clear a table: - -```php -call(function () { - DB::table('recent_users')->delete(); - })->daily(); - } -} -``` - -In addition to scheduling using closures, you may also schedule [invokable objects](https://secure.php.net/manual/en/language.oop5.magic.php#object.invoke). Invokable objects are simple PHP classes that contain an `__invoke` method: - -```php -$schedule->call(new DeleteRecentUsers)->daily(); -``` - -If you would like to view an overview of your scheduled tasks and the next time they are scheduled to run, you may use the `schedule:list` Artisan command: - -```bash -php artisan schedule:list -``` - -### Scheduling Artisan Commands - -In addition to scheduling closures, you may also schedule [Artisan commands](/docs/artisan) and system commands. For example, you may use the `command` method to schedule an Artisan command using either the command's name or class. - -When scheduling Artisan commands using the command's class name, you may pass an array of additional command-line arguments that should be provided to the command when it is invoked: - -```php -use App\Console\Commands\SendEmailsCommand; - -$schedule->command('emails:send Taylor --force')->daily(); - -$schedule->command(SendEmailsCommand::class, ['Taylor', '--force'])->daily(); -``` - -### Scheduling Queued Jobs - -The `job` method may be used to schedule a [queued job](/docs/queues). This method provides a convenient way to schedule queued jobs without using the `call` method to define closures to queue the job: - -```php -use App\Jobs\Heartbeat; - -$schedule->job(new Heartbeat)->everyFiveMinutes(); -``` - -Optional second and third arguments may be provided to the `job` method which specifies the queue name and queue connection that should be used to queue the job: - -```php -use App\Jobs\Heartbeat; - -// Dispatch the job to the "heartbeats" queue on the "sqs" connection... -$schedule->job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes(); -``` - -### Scheduling Shell Commands - -The `exec` method may be used to issue a command to the operating system: - -```php -$schedule->exec('node /home/forge/script.js')->daily(); -``` - -### Schedule Frequency Options - -We've already seen a few examples of how you may configure a task to run at specified intervals. However, there are many more task schedule frequencies that you may assign to a task: - -
      - -Method | Description -------------- | ------------- -`->cron('* * * * *');` | Run the task on a custom cron schedule -`->everySecond();` | Run the task every second -`->everyTwoSeconds();` | Run the task every two seconds -`->everyFiveSeconds();` | Run the task every five seconds -`->everyTenSeconds();` | Run the task every ten seconds -`->everyFifteenSeconds();` | Run the task every fifteen seconds -`->everyTwentySeconds();` | Run the task every twenty seconds -`->everyThirtySeconds();` | Run the task every thirty seconds -`->everyMinute();` | Run the task every minute -`->everyTwoMinutes();` | Run the task every two minutes -`->everyThreeMinutes();` | Run the task every three minutes -`->everyFourMinutes();` | Run the task every four minutes -`->everyFiveMinutes();` | Run the task every five minutes -`->everyTenMinutes();` | Run the task every ten minutes -`->everyFifteenMinutes();` | Run the task every fifteen minutes -`->everyThirtyMinutes();` | Run the task every thirty minutes -`->hourly();` | Run the task every hour -`->hourlyAt(17);` | Run the task every hour at 17 minutes past the hour -`->everyOddHour($minutes = 0);` | Run the task every odd hour -`->everyTwoHours($minutes = 0);` | Run the task every two hours -`->everyThreeHours($minutes = 0);` | Run the task every three hours -`->everyFourHours($minutes = 0);` | Run the task every four hours -`->everySixHours($minutes = 0);` | Run the task every six hours -`->daily();` | Run the task every day at midnight -`->dailyAt('13:00');` | Run the task every day at 13:00 -`->twiceDaily(1, 13);` | Run the task daily at 1:00 & 13:00 -`->twiceDailyAt(1, 13, 15);` | Run the task daily at 1:15 & 13:15 -`->weekly();` | Run the task every Sunday at 00:00 -`->weeklyOn(1, '8:00');` | Run the task every week on Monday at 8:00 -`->monthly();` | Run the task on the first day of every month at 00:00 -`->monthlyOn(4, '15:00');` | Run the task every month on the 4th at 15:00 -`->twiceMonthly(1, 16, '13:00');` | Run the task monthly on the 1st and 16th at 13:00 -`->lastDayOfMonth('15:00');` | Run the task on the last day of the month at 15:00 -`->quarterly();` | Run the task on the first day of every quarter at 00:00 -`->quarterlyOn(4, '14:00');` | Run the task every quarter on the 4th at 14:00 -`->yearly();` | Run the task on the first day of every year at 00:00 -`->yearlyOn(6, 1, '17:00');` | Run the task every year on June 1st at 17:00 -`->timezone('America/New_York');` | Set the timezone for the task - -
      - -These methods may be combined with additional constraints to create even more finely tuned schedules that only run on certain days of the week. For example, you may schedule a command to run weekly on Monday: - -```php -// Run once per week on Monday at 1 PM... -$schedule->call(function () { - // ... -})->weekly()->mondays()->at('13:00'); - -// Run hourly from 8 AM to 5 PM on weekdays... -$schedule->command('foo') - ->weekdays() - ->hourly() - ->timezone('America/Chicago') - ->between('8:00', '17:00'); -``` - -A list of additional schedule constraints may be found below: - -
      - -Method | Description -------------- | ------------- -`->weekdays();` | Limit the task to weekdays -`->weekends();` | Limit the task to weekends -`->sundays();` | Limit the task to Sunday -`->mondays();` | Limit the task to Monday -`->tuesdays();` | Limit the task to Tuesday -`->wednesdays();` | Limit the task to Wednesday -`->thursdays();` | Limit the task to Thursday -`->fridays();` | Limit the task to Friday -`->saturdays();` | Limit the task to Saturday -`->days(array\|mixed);` | Limit the task to specific days -`->between($startTime, $endTime);` | Limit the task to run between start and end times -`->unlessBetween($startTime, $endTime);` | Limit the task to not run between start and end times -`->when(Closure);` | Limit the task based on a truth test -`->environments($env);` | Limit the task to specific environments - -
      - -#### Day Constraints - -The `days` method may be used to limit the execution of a task to specific days of the week. For example, you may schedule a command to run hourly on Sundays and Wednesdays: - -```php -$schedule->command('emails:send') - ->hourly() - ->days([0, 3]); -``` - -Alternatively, you may use the constants available on the `Hypervel\Console\Scheduling\Schedule` class when defining the days on which a task should run: - -```php -use Hypervel\Console\Scheduling\Schedule; - -$schedule->command('emails:send') - ->hourly() - ->days([Schedule::SUNDAY, Schedule::WEDNESDAY]); -``` - -#### Between Time Constraints - -The `between` method may be used to limit the execution of a task based on the time of day: - -```php -$schedule->command('emails:send') - ->hourly() - ->between('7:00', '22:00'); -``` - -Similarly, the `unlessBetween` method can be used to exclude the execution of a task for a period of time: - -```php -$schedule->command('emails:send') - ->hourly() - ->unlessBetween('23:00', '4:00'); -``` - -#### Truth Test Constraints - -The `when` method may be used to limit the execution of a task based on the result of a given truth test. In other words, if the given closure returns `true`, the task will execute as long as no other constraining conditions prevent the task from running: - -```php -$schedule->command('emails:send')->daily()->when(function () { - return true; -}); -``` - -The `skip` method may be seen as the inverse of `when`. If the `skip` method returns `true`, the scheduled task will not be executed: - -```php -$schedule->command('emails:send')->daily()->skip(function () { - return true; -}); -``` - -When using chained `when` methods, the scheduled command will only execute if all `when` conditions return `true`. - -#### Environment Constraints - -The `environments` method may be used to execute tasks only on the given environments (as defined by the `APP_ENV` [environment variable](/docs/configuration#environment-configuration)): - -```php -$schedule->command('emails:send') - ->daily() - ->environments(['staging', 'production']); -``` - -### Timezones - -Using the `timezone` method, you may specify that a scheduled task's time should be interpreted within a given timezone: - -```php -$schedule->command('report:generate') - ->timezone('America/New_York') - ->at('2:00') -``` - -If you are repeatedly assigning the same timezone to all of your scheduled tasks, you may wish to define a `scheduleTimezone` method in your `App\Console\Kernel` class. This method should return the default timezone that should be assigned to all scheduled tasks: - -```php -use DateTimeZone; - -/** - * Get the timezone that should be used by default for scheduled events. - */ -protected function scheduleTimezone(): DateTimeZone|string|null -{ - return 'America/Chicago'; -} -``` - -::: warning -Remember that some timezones utilize daylight savings time. When daylight saving time changes occur, your scheduled task may run twice or even not run at all. For this reason, we recommend avoiding timezone scheduling when possible. -::: - -### Preventing Task Overlaps - -By default, scheduled tasks will be run even if the previous instance of the task is still running. To prevent this, you may use the `withoutOverlapping` method: - -```php -$schedule->command('emails:send')->withoutOverlapping(); -``` - -In this example, the `emails:send` [Artisan command](/docs/artisan) will be run every minute if it is not already running. The `withoutOverlapping` method is especially useful if you have tasks that vary drastically in their execution time, preventing you from predicting exactly how long a given task will take. - -If needed, you may specify how many minutes must pass before the "without overlapping" lock expires. By default, the lock will expire after 24 hours: - -```php -$schedule->command('emails:send')->withoutOverlapping(10); -``` - -Behind the scenes, the `withoutOverlapping` method utilizes your application's [cache](/docs/cache) to obtain locks. If necessary, you can clear these cache locks using the `schedule:clear-cache` Artisan command. This is typically only necessary if a task becomes stuck due to an unexpected server problem. - -### Running Tasks on One Server - -::: warning -To utilize this feature, your application must be using the `database`, `memcached`, `dynamodb`, or `redis` cache driver as your application's default cache driver. In addition, all servers must be communicating with the same central cache server. -::: - -If your application's scheduler is running on multiple servers, you may limit a scheduled job to only execute on a single server. For instance, assume you have a scheduled task that generates a new report every Friday night. If the task scheduler is running on three worker servers, the scheduled task will run on all three servers and generate the report three times. Not good! - -To indicate that the task should run on only one server, use the `onOneServer` method when defining the scheduled task. The first server to obtain the task will secure an atomic lock on the job to prevent other servers from running the same task at the same time: - -```php -$schedule->command('report:generate') - ->fridays() - ->at('17:00') - ->onOneServer(); -``` - -#### Naming Single Server Jobs - -Sometimes you may need to schedule the same job to be dispatched with different parameters, while still instructing Hypervel to run each permutation of the job on a single server. To accomplish this, you may assign each schedule definition a unique name via the `name` method: - -```php -$schedule->job(new CheckUptime('https://hypervel.org')) - ->name('check_uptime:hypervel.org') - ->everyFiveMinutes() - ->onOneServer(); - -$schedule->job(new CheckUptime('https://hypervel.org')) - ->name('check_uptime:hypervel.org') - ->everyFiveMinutes() - ->onOneServer(); -``` - -Similarly, scheduled closures must be assigned a name if they are intended to be run on one server: - -```php -$schedule->call(fn () => User::resetApiRequestCount()) - ->name('reset-api-request-count') - ->daily() - ->onOneServer(); -``` - -### Background Tasks - -By default, multiple tasks scheduled at the same time will execute sequentially based on the order they are defined in your `schedule` method. If you have long-running tasks, this may cause subsequent tasks to start much later than anticipated. If you would like to run tasks in the background so that they may all run simultaneously, you may use the `runInBackground` method: - -```php -$schedule->command('analytics:report') - ->daily() - ->runInBackground(); -``` - -::: warning -The `runInBackground` method may only be used when scheduling tasks via the `command` and `exec` methods. -::: - -::: info -Unlike in Laravel, all the background tasks are executed in coroutines. It's much more efficient when you need to run many background tasks simultaneously in intensive I/O scenes. -::: - -## Running the Scheduler - -Now that we have learned how to define scheduled tasks, let's discuss how to actually run them on our server. The `schedule:run` Artisan command will evaluate all of your scheduled tasks and determine if they need to run based on the server's current time. - -So, when using Hypervel's scheduler, we only need to run the `schedule:run` command and a scheduler will be running and waiting for the upcoming scheduling tasks constantly. - -```shell -php artisan schedule:run -``` - -::: warning -In Laravel, `schedule:run` is designed for running in system's cron jobs. However, in Hypervel it's designed as a long-running process. -::: - - -If you prefer to run the scheduler in traditional system's scheduler, you can also add a single cron configuration entry to our server that runs the `schedule:run --once` command every minute. - -```shell -* * * * * cd /path-to-your-project && php artisan schedule:run --once >> /dev/null 2>&1 -``` - -#### Concurrent Processing in the Scheduler - -Different from Laravel, all the background processes will be executed in coroutines instead of processes. The majority of background processing is I/O bound scenario. Running in tasks in coroutines is much more efficient than processes. - -By default the concurrency number for coroutines is set to `60`, which means you can have 60 running coroutines at the same time. You can configure this number when you run the scheduling command: - -```shell:no-line-numbers -php artisan schedule:run --concurrency=100 -``` - -For CPU-bound queue jobs, coroutines offer limited performance benefits. You can run these tasks using system commands using `exec`, then the task will be executed in a new background process: - -```php -$schedule->exec('php artisan calculate-command') - ->runInBackground(); -``` - -### Sub-Minute Scheduled Tasks - -On most operating systems, cron jobs are limited to running a maximum of once per minute. However, Hypervel's scheduler allows you to schedule tasks to run at more frequent intervals, even as often as once per second: - -```php -$schedule->call(function () { - DB::table('recent_users')->delete(); -})->everySecond(); -``` - -### Stopping Tasks - -As the `schedule:run` command runs constantly for handling upcoming tasks, you may sometimes need to stop the command when deploying your application. - -To stop in-progress `schedule:run` invocations, you may add the `schedule:stop` command to your application's deployment script. This command should be invoked after your application is finished deploying: - -```shell -php artisan schedule:stop -``` - -After receiving a stop signal, the scheduler will wait for unfinished tasks to complete before exiting. - -## Task Output - -The Hypervel scheduler provides several convenient methods for working with the output generated by scheduled tasks. First, using the `sendOutputTo` method, you may send the output to a file for later inspection: - -```php -$schedule->command('emails:send') - ->daily() - ->sendOutputTo($filePath); -``` - -If you would like to append the output to a given file, you may use the `appendOutputTo` method: - -```php -$schedule->command('emails:send') - ->daily() - ->appendOutputTo($filePath); -``` - -Using the `emailOutputTo` method, you may email the output to an email address of your choice. Before emailing the output of a task, you should configure Hypervel's [email services](/docs/mail): - -```php -$schedule->command('report:generate') - ->daily() - ->sendOutputTo($filePath) - ->emailOutputTo('taylor@example.com'); -``` - -If you only want to email the output if the scheduled Artisan or system command terminates with a non-zero exit code, use the `emailOutputOnFailure` method: - -```php -$schedule->command('report:generate') - ->daily() - ->emailOutputOnFailure('taylor@example.com'); -``` - -::: warning -The `emailOutputTo`, `emailOutputOnFailure`, `sendOutputTo`, and `appendOutputTo` methods are exclusive to the `command` and `exec` methods. -::: - -## Task Hooks - -Using the `before` and `after` methods, you may specify code to be executed before and after the scheduled task is executed: - -```php -$schedule->command('emails:send') - ->daily() - ->before(function () { - // The task is about to execute... - }) - ->after(function () { - // The task has executed... - }); -``` - -The `onSuccess` and `onFailure` methods allow you to specify code to be executed if the scheduled task succeeds or fails. A failure indicates that the scheduled Artisan or system command terminated with a non-zero exit code: - -```php -$schedule->command('emails:send') - ->daily() - ->onSuccess(function () { - // The task succeeded... - }) - ->onFailure(function () { - // The task failed... - }); -``` - -If output is available from your command, you may access it in your `after`, `onSuccess` or `onFailure` hooks by type-hinting an `Hypervel\Support\Stringable` instance as the `$output` argument of your hook's closure definition: - -```php -use Hypervel\Support\Stringable; - -$schedule->command('emails:send') - ->daily() - ->onSuccess(function (Stringable $output) { - // The task succeeded... - }) - ->onFailure(function (Stringable $output) { - // The task failed... - }); -``` - -#### Pinging URLs - -Using the `pingBefore` and `thenPing` methods, the scheduler can automatically ping a given URL before or after a task is executed. This method is useful for notifying an external service, such as [Healthchecks](https://healthchecks.io/), that your scheduled task is beginning or has finished execution: - -```php -$schedule->command('emails:send') - ->daily() - ->pingBefore($url) - ->thenPing($url); -``` - -The `pingBeforeIf` and `thenPingIf` methods may be used to ping a given URL only if a given condition is `true`: - -```php -$schedule->command('emails:send') - ->daily() - ->pingBeforeIf($condition, $url) - ->thenPingIf($condition, $url); -``` - -The `pingOnSuccess` and `pingOnFailure` methods may be used to ping a given URL only if the task succeeds or fails. A failure indicates that the scheduled Artisan or system command terminated with a non-zero exit code: - -```php -$schedule->command('emails:send') - ->daily() - ->pingOnSuccess($successUrl) - ->pingOnFailure($failureUrl); -``` - -All of the ping methods require the Guzzle HTTP library. Guzzle is typically installed in all new Hypervel projects by default, but, you may manually install Guzzle into your project using the Composer package manager if it has been accidentally removed: - -```shell -composer require guzzlehttp/guzzle -``` - -## Events - -If needed, you may listen to [events](/docs/events) dispatched by the scheduler. Typically, event listener mappings will be defined within your application's `App\Providers\EventServiceProvider` class: - -```php -/** - * The event listener mappings for the application. - */ -protected array $listen = [ - 'Hypervel\Console\Events\ScheduledTaskStarting' => [ - 'App\Listeners\LogScheduledTaskStarting', - ], - - 'Hypervel\Console\Events\ScheduledTaskFinished' => [ - 'App\Listeners\LogScheduledTaskFinished', - ], - - 'Hypervel\Console\Events\ScheduledTaskSkipped' => [ - 'App\Listeners\LogScheduledTaskSkipped', - ], - - 'Hypervel\Console\Events\ScheduledTaskFailed' => [ - 'App\Listeners\LogScheduledTaskFailed', - ], -]; -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/seeding.md b/_archive/docs/0.3/docs/seeding.md deleted file mode 100644 index 3737a69b0a..0000000000 --- a/_archive/docs/0.3/docs/seeding.md +++ /dev/null @@ -1,114 +0,0 @@ -# Database: Seeding -[[toc]] - -## Introduction - -Hypervel includes the ability to seed your database with data using seed classes. All seed classes are stored in the `database/seeders` directory. By default, a `DatabaseSeeder` class is defined for you. From this class, you may use the `call` method to run other seed classes, allowing you to control the seeding order. - -::: note -[Mass assignment protection](/docs/eloquent#mass-assignment) is automatically disabled during database seeding. -::: - -## Writing Seeders - -To generate a seeder, execute the `make:seeder` [Artisan command](/docs/artisan). All seeders generated by the framework will be placed in the `database/seeders` directory: - -```shell:no-line-numbers -php artisan make:seeder UserSeeder -``` - -A seeder class only contains one method by default: `run`. This method is called when the `db:seed` [Artisan command](/docs/artisan) is executed. Within the `run` method, you may insert data into your database however you wish. You may use the [query builder](/docs/queries) to manually insert data or you may use [Eloquent model factories](/docs/eloquent-factories). - -As an example, let's modify the default `DatabaseSeeder` class and add a database insert statement to the `run` method: - -```php -insert([ - 'name' => Str::random(10), - 'email' => Str::random(10).'@example.com', - 'password' => Hash::make('password'), - ]); - } -} -``` - -::: note -You may type-hint any dependencies you need within the `run` method's signature. They will automatically be resolved via the Hypervel [service container](/docs/container). -::: - -### Using Model Factories - -Of course, manually specifying the attributes for each model seed is cumbersome. Instead, you can use [model factories](/docs/eloquent-factories) to conveniently generate large amounts of database records. First, review the [model factory documentation](/docs/eloquent-factories) to learn how to define your factories. - -For example, let's create 50 users: - -```php -use App\Models\User; - -/** - * Run the database seeders. - */ -public function run(): void -{ - User::factory()->create(); -} -``` - -### Calling Additional Seeders - -Within the `DatabaseSeeder` class, you may use the `call` method to execute additional seed classes. Using the `call` method allows you to break up your database seeding into multiple files so that no single seeder class becomes too large. The `call` method accepts an array of seeder classes that should be executed: - -```php -/** - * Run the database seeders. - */ -public function run(): void -{ - $this->call([ - UserSeeder::class, - PostSeeder::class, - CommentSeeder::class, - ]); -} -``` - -## Running Seeders - -You may execute the `db:seed` Artisan command to seed your database. By default, the `db:seed` command runs the `Database\Seeders\DatabaseSeeder` class, which may in turn invoke other seed classes. However, you may use the `--class` option to specify a specific seeder class to run individually: - -```shell:no-line-numbers -php artisan db:seed - -php artisan db:seed --class=UserSeeder -``` - -You may also seed your database using the `migrate:fresh` command in combination with the `--seed` option, which will drop all tables and re-run all of your migrations. This command is useful for completely re-building your database. The `--seeder` option may be used to specify a specific seeder to run: - -```shell:no-line-numbers -php artisan migrate:fresh --seed - -php artisan migrate:fresh --seed --seeder=UserSeeder -``` - -#### Forcing Seeders to Run in Production - -Some seeding operations may cause you to alter or lose data. In order to protect you from running seeding commands against your production database, you will be prompted for confirmation before the seeders are executed in the `production` environment. To force the seeders to run without a prompt, use the `--force` flag: - -```shell:no-line-numbers -php artisan db:seed --force -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/session.md b/_archive/docs/0.3/docs/session.md deleted file mode 100644 index c30cf8f076..0000000000 --- a/_archive/docs/0.3/docs/session.md +++ /dev/null @@ -1,373 +0,0 @@ -# Session -[[toc]] - -## Introduction - -Since HTTP driven applications are stateless, sessions provide a way to store information about the user across multiple requests. That user information is typically placed in a persistent store / backend that can be accessed from subsequent requests. - -Hypervel ships with a variety of session backends that are accessed through an expressive, unified API. Support for popular backends such as [Redis](https://redis.io), file, and databases is included. - -### Configuration - -Your application's session configuration file is stored at `config/session.php`. Be sure to review the options available to you in this file. By default, Hypervel is configured to use the `file` session driver, which will work well for many applications. If your application will be load balanced across multiple web servers, you a centralized store that all servers can access, such as Redis or a database. - -::: info -It's recommended to use `redis` as the session driver if possible, it will have better performance and scalability. -::: - -The session `driver` configuration option defines where session data will be stored for each request. Hypervel ships with some great drivers out of the box: - -- `file` - sessions are stored in `storage/framework/sessions`. -- `cookie` - sessions are stored in secure cookies. -- `database` - sessions are stored in a relational database. -- `redis` - sessions are stored in redis, a fast and cache based store. -- `array` - sessions are stored in a PHP array and will not be persisted. - -::: important -Before using session, you need to enable the `Hypervel\Session\Middleware\StartSession` middleware in `Kernel.php`. -::: - -::: tip -The array driver is primarily used during [testing](/docs/testing) and prevents the data stored in the session from being persisted. -::: - -### Driver Prerequisites - -#### Database - -When using the `database` session driver, you will need to create a table to contain the session records. An example `Schema` declaration for the table may be found below: - -```php -use Hyperf\Database\Schema\Blueprint; -use Hypervel\Support\Facades\Schema; - -Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); -}); -``` - -You may use the `session:table` Artisan command to generate this migration. To learn more about database migrations, you may consult the complete [migration documentation](/docs/migrations): - -```shell -php artisan session:table - -php artisan migrate -``` - -#### Redis - -Before using Redis sessions with Hypervel, you will need to either install the PhpRedis PHP extension via PECL. For more information on configuring Redis, consult Hypervel's [Redis documentation](/docs/redis#configuration). - -::: note -In the `session` configuration file, the `connection` option may be used to specify which Redis connection is used by the session. -::: - -## Interacting With the Session - -### Retrieving Data - -There are two primary ways of working with session data in Hypervel: the global `session` helper and via a `Request` instance. First, let's look at accessing the session via a `Request` instance, which can be type-hinted on a route closure or controller method. Remember, controller method dependencies are automatically injected via the Hypervel [service container](/docs/container): - -```php -session()->get('key'); - - // ... - - $user = $this->users->find($id); - - return view('user.profile', ['user' => $user]); - } -} -``` - -When you retrieve an item from the session, you may also pass a default value as the second argument to the `get` method. This default value will be returned if the specified key does not exist in the session. If you pass a closure as the default value to the `get` method and the requested key does not exist, the closure will be executed and its result returned: - -```php -$value = $request->session()->get('key', 'default'); - -$value = $request->session()->get('key', function () { - return 'default'; -}); -``` - -#### The Global Session Helper - -You may also use the global `session` PHP function to retrieve and store data in the session. When the `session` helper is called with a single, string argument, it will return the value of that session key. When the helper is called with an array of key / value pairs, those values will be stored in the session: - -```php -Route::get('/home', function () { - // Retrieve a piece of data from the session... - $value = session('key'); - - // Specifying a default value... - $value = session('key', 'default'); - - // Store a piece of data in the session... - session(['key' => 'value']); -}); -``` - -::: tip -There is little practical difference between using the session via an HTTP request instance versus using the global `session` helper. Both methods are [testable](/docs/testing) via the `assertSessionHas` method which is available in all of your test cases. -::: - -#### Retrieving All Session Data - -If you would like to retrieve all the data in the session, you may use the `all` method: - -```php -$data = $request->session()->all(); -``` - -#### Retrieving a Portion of the Session Data - -The `only` and `except` methods may be used to retrieve a subset of the session data: - -```php -$data = $request->session()->only(['username', 'email']); - -$data = $request->session()->except(['username', 'email']); -``` - -#### Determining if an Item Exists in the Session - -To determine if an item is present in the session, you may use the `has` method. The `has` method returns `true` if the item is present and is not `null`: - -```php -if ($request->session()->has('users')) { - // ... -} -``` - -To determine if an item is present in the session, even if its value is `null`, you may use the `exists` method: - -```php -if ($request->session()->exists('users')) { - // ... -} -``` - -To determine if an item is not present in the session, you may use the `missing` method. The `missing` method returns true if the item is not present: - -```php -if ($request->session()->missing('users')) { - // ... -} -``` - -### Storing Data - -To store data in the session, you will typically use the request instance's `put` method or the global `session` helper: - -```php -// Via a request instance... -$request->session()->put('key', 'value'); - -// Via the global "session" helper... -session(['key' => 'value']); -``` - -#### Pushing to Array Session Values - -The `push` method may be used to push a new value onto a session value that is an array. For example, if the `user.teams` key contains an array of team names, you may push a new value onto the array like so: - -```php -$request->session()->push('user.teams', 'developers'); -``` - -#### Retrieving and Deleting an Item - -The `remove` method will retrieve and delete an item from the session in a single statement: - -```php -$value = $request->session()->pull('key', 'default'); -``` - -#### Incrementing and Decrementing Session Values - -If your session data contains an integer you wish to increment or decrement, you may use the `increment` and `decrement` methods: - -```php -$request->session()->increment('count'); - -$request->session()->increment('count', $incrementBy = 2); - -$request->session()->decrement('count'); - -$request->session()->decrement('count', $decrementBy = 2); -``` - -### Flash Data - -Sometimes you may wish to store items in the session for the next request. You may do so using the `flash` method. Data stored in the session using this method will be available immediately and during the subsequent HTTP request. After the subsequent HTTP request, the flashed data will be deleted. Flash data is primarily useful for short-lived status messages: - -```php -$request->session()->flash('status', 'Task was successful!'); -``` - -If you need to persist your flash data for several requests, you may use the `reflash` method, which will keep all of the flash data for an additional request. If you only need to keep specific flash data, you may use the `keep` method: - -```php -$request->session()->reflash(); - -$request->session()->keep(['username', 'email']); -``` - -To persist your flash data only for the current request, you may use the `now` method: - -```php -$request->session()->now('status', 'Task was successful!'); -``` - -### Deleting Data - -The `forget` method will remove a piece of data from the session. If you would like to remove all data from the session, you may use the `flush` method: - -```php -// Forget a single key... -$request->session()->forget('name'); - -// Forget multiple keys... -$request->session()->forget(['name', 'status']); - -$request->session()->flush(); -``` - -### Regenerating the Session ID - -Regenerating the session ID is often done in order to prevent malicious users from exploiting a [session fixation](https://owasp.org/www-community/attacks/Session_fixation) attack on your application. - -If you need to manually regenerate the session ID, you may use the `regenerate` method: - -```php -$request->session()->regenerate(); -``` - -If you need to regenerate the session ID and remove all data from the session in a single statement, you may use the `invalidate` method: - -```php -$request->session()->invalidate(); -``` - -## Session Blocking - -::: warning -To utilize session blocking, your application must be using a cache driver that supports [atomic locks](/docs/cache#atomic-locks). Currently, those cache drivers include the `redis`, `database`, `file`, and `array` drivers. In addition, you may not use the `cookie` session driver. -::: - -By default, Hypervel allows requests using the same session to execute concurrently. So, for example, if you use a JavaScript HTTP library to make two HTTP requests to your application, they will both execute at the same time. For many applications, this is not a problem; however, session data loss can occur in a small subset of applications that make concurrent requests to two different application endpoints which both write data to the session. - -To mitigate this, Hypervel provides functionality that allows you to limit concurrent requests for a given session. To get started, you may simply set the `block` option onto your route definition. In this example, an incoming request to the `/profile` endpoint would acquire a session lock. While this lock is being held, any incoming requests to the `/profile` or `/order` endpoints which share the same session ID will wait for the first request to finish executing before continuing their execution: - -```php -Route::post('/profile', function () { - // ... -}, ['block' => ['lock' => 10, 'wait' => 10]]); - -Route::post('/order', function () { - // ... -}, ['block' => ['lock' => 10, 'wait' => 10]]); -``` - -The `block` option accepts two optional configs. The `lock` config accepted by the `block` method is the maximum number of seconds the session lock should be held for before it is released. Of course, if the request finishes executing before this time the lock will be released earlier. - -The `wait` config accepted by the `block` method is the number of seconds a request should wait while attempting to obtain a session lock. An `Hypervel\Cache\Exceptions` will be thrown if the request is unable to obtain a session lock within the given number of seconds. - -If neither of these arguments is passed, the lock will be obtained for a maximum of 10 seconds and requests will wait a maximum of 10 seconds while attempting to obtain a lock: - -```php -Route::post('/profile', function () { - // ... -}, ['block' => true]); -``` - -## Adding Custom Session Drivers - -### Implementing the Driver - -If none of the existing session drivers fit your application's needs, Laravel makes it possible to write your own session handler. Your custom session driver should implement PHP's built-in `SessionHandlerInterface`. This interface contains just a few simple methods. A stubbed MongoDB implementation looks like the following: - -```php - [ - 'client_id' => env('GITHUB_CLIENT_ID'), - 'client_secret' => env('GITHUB_CLIENT_SECRET'), - 'redirect' => 'http://example.com/callback-url', -], -``` - -::: note -If the `redirect` option contains a relative path, it will automatically be resolved to a fully qualified URL. -::: - -## Authentication - -### Routing - -To authenticate users using an OAuth provider, you will need two routes: one for redirecting the user to the OAuth provider, and another for receiving the callback from the provider after authentication. The example routes below demonstrate the implementation of both routes: - -```php -use Hypervel\Socialite\Facades\Socialite; - -Route::get('/auth/redirect', function () { - return Socialite::driver('github')->redirect(); -}); - -Route::get('/auth/callback', function () { - $user = Socialite::driver('github')->user(); - - // $user->token -}); -``` - -The `redirect` method provided by the `Socialite` facade takes care of redirecting the user to the OAuth provider, while the `user` method will examine the incoming request and retrieve the user's information from the provider after they have approved the authentication request. - -### Authentication and Storage - -Once the user has been retrieved from the OAuth provider, you may determine if the user exists in your application's database and [authenticate the user](/docs/authentication#authenticate-a-user-instance). If the user does not exist in your application's database, you will typically create a new record in your database to represent the user: - -```php -use App\Models\User; -use Hypervel\Support\Facades\Auth; -use Hypervel\Socialite\Facades\Socialite; - -Route::get('/auth/callback', function () { - $githubUser = Socialite::driver('github')->user(); - - $user = User::updateOrCreate([ - 'github_id' => $githubUser->id, - ], [ - 'name' => $githubUser->name, - 'email' => $githubUser->email, - 'github_token' => $githubUser->token, - 'github_refresh_token' => $githubUser->refreshToken, - ]); - - Auth::login($user); - - return redirect('/dashboard'); -}); -``` - -::: note -For more information regarding what user information is available from specific OAuth providers, please consult the documentation on [retrieving user details](#retrieving-user-details). -::: - -### Access Scopes - -Before redirecting the user, you may use the `scopes` method to specify the "scopes" that should be included in the authentication request. This method will merge all previously specified scopes with the scopes that you specify: - -```php -use Hypervel\Socialite\Facades\Socialite; - -return Socialite::driver('github') - ->scopes(['read:user', 'public_repo']) - ->redirect(); -``` - -You can overwrite all existing scopes on the authentication request using the `setScopes` method: - -```php -return Socialite::driver('github') - ->setScopes(['read:user', 'public_repo']) - ->redirect(); -``` - -### Slack Bot Scopes - -Slack's API provides [different types of access tokens](https://api.slack.com/authentication/token-types), each with their own set of [permission scopes](https://api.slack.com/scopes). Socialite is compatible with both of the following Slack access tokens types: - -
      - -- Bot (prefixed with `xoxb-`) -- User (prefixed with `xoxp-`) - -
      - -By default, the `slack` driver will generate a `user` token and invoking the driver's `user` method will return the user's details. - -Bot tokens are primarily useful if your application will be sending notifications to external Slack workspaces that are owned by your application's users. To generate a bot token, invoke the `asBotUser` method before redirecting the user to Slack for authentication: - -```php -return Socialite::driver('slack') - ->asBotUser() - ->setScopes(['chat:write', 'chat:write.public', 'chat:write.customize']) - ->redirect(); -``` - -In addition, you must invoke the `asBotUser` method before invoking the `user` method after Slack redirects the user back to your application after authentication: - -```php -$user = Socialite::driver('slack')->asBotUser()->user(); -``` - -When generating a bot token, the `user` method will still return a `Hypervel\Socialite\Two\User` instance; however, only the `token` property will be hydrated. This token may be stored in order to [send notifications to the authenticated user's Slack workspaces](/docs/notifications#notifying-external-slack-workspaces). - -### Optional Parameters - -A number of OAuth providers support other optional parameters on the redirect request. To include any optional parameters in the request, call the `with` method with an associative array: - -```php -use Hypervel\Socialite\Facades\Socialite; - -return Socialite::driver('google') - ->with(['hd' => 'example.com']) - ->redirect(); -``` - -::: warning -When using the `with` method, be careful not to pass any reserved keywords such as `state` or `response_type`. -::: - -## Retrieving User Details - -After the user is redirected back to your application's authentication callback route, you may retrieve the user's details using Socialite's `user` method. The user object returned by the `user` method provides a variety of properties and methods you may use to store information about the user in your own database. - -Differing properties and methods may be available on this object depending on whether the OAuth provider you are authenticating with supports OAuth 1.0 or OAuth 2.0: - -```php -use Hypervel\Socialite\Facades\Socialite; - -Route::get('/auth/callback', function () { - $user = Socialite::driver('github')->user(); - - // OAuth 2.0 providers... - $token = $user->token; - $refreshToken = $user->refreshToken; - $expiresIn = $user->expiresIn; - - // OAuth 1.0 providers... - $token = $user->token; - $tokenSecret = $user->tokenSecret; - - // All providers... - $user->getId(); - $user->getNickname(); - $user->getName(); - $user->getEmail(); - $user->getAvatar(); -}); -``` - -#### Retrieving User Details From a Token - -If you already have a valid access token for a user, you can retrieve their user details using Socialite's `userFromToken` method: - -```php -use Hypervel\Socialite\Facades\Socialite; - -$user = Socialite::driver('github')->userFromToken($token); -``` - -If you are using Facebook Limited Login via an iOS application, Facebook will return an OIDC token instead of an access token. Like an access token, the OIDC token can be provided to the `userFromToken` method in order to retrieve user details. - -#### Stateless Authentication - -The `stateless` method may be used to disable session state verification. This is useful when adding social authentication to a stateless API that does not utilize cookie based sessions: - -```php -use Hypervel\Socialite\Facades\Socialite; - -return Socialite::driver('google')->stateless()->user(); -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/strings.md b/_archive/docs/0.3/docs/strings.md deleted file mode 100644 index 467da0f754..0000000000 --- a/_archive/docs/0.3/docs/strings.md +++ /dev/null @@ -1,3399 +0,0 @@ -# Strings -[[toc]] - - -## Introduction - -Hypervel includes a variety of functions for manipulating string values. Many of these functions are used by the framework itself; however, you are free to use them in your own applications if you find them convenient. - - -## Available Methods - - - - -### Strings - -
      - -[\__](#method-__) -[class_basename](#method-class-basename) -[e](#method-e) -[preg_replace_array](#method-preg-replace-array) -[Str::after](#method-str-after) -[Str::afterLast](#method-str-after-last) -[Str::apa](#method-str-apa) -[Str::ascii](#method-str-ascii) -[Str::before](#method-str-before) -[Str::beforeLast](#method-str-before-last) -[Str::between](#method-str-between) -[Str::betweenFirst](#method-str-between-first) -[Str::camel](#method-camel-case) -[Str::charAt](#method-char-at) -[Str::chopStart](#method-str-chop-start) -[Str::chopEnd](#method-str-chop-end) -[Str::contains](#method-str-contains) -[Str::containsAll](#method-str-contains-all) -[Str::doesntContain](#method-str-doesnt-contain) -[Str::endsWith](#method-ends-with) -[Str::excerpt](#method-excerpt) -[Str::finish](#method-str-finish) -[Str::headline](#method-str-headline) -[Str::is](#method-str-is) -[Str::isAscii](#method-str-is-ascii) -[Str::isJson](#method-str-is-json) -[Str::isUlid](#method-str-is-ulid) -[Str::isUrl](#method-str-is-url) -[Str::isUuid](#method-str-is-uuid) -[Str::kebab](#method-kebab-case) -[Str::lcfirst](#method-str-lcfirst) -[Str::length](#method-str-length) -[Str::limit](#method-str-limit) -[Str::lower](#method-str-lower) -[Str::mask](#method-str-mask) -[Str::orderedUuid](#method-str-ordered-uuid) -[Str::padBoth](#method-str-padboth) -[Str::padLeft](#method-str-padleft) -[Str::padRight](#method-str-padright) -[Str::password](#method-str-password) -[Str::plural](#method-str-plural) -[Str::pluralStudly](#method-str-plural-studly) -[Str::position](#method-str-position) -[Str::random](#method-str-random) -[Str::remove](#method-str-remove) -[Str::repeat](#method-str-repeat) -[Str::replace](#method-str-replace) -[Str::replaceArray](#method-str-replace-array) -[Str::replaceFirst](#method-str-replace-first) -[Str::replaceLast](#method-str-replace-last) -[Str::replaceMatches](#method-str-replace-matches) -[Str::replaceStart](#method-str-replace-start) -[Str::replaceEnd](#method-str-replace-end) -[Str::reverse](#method-str-reverse) -[Str::singular](#method-str-singular) -[Str::slug](#method-str-slug) -[Str::snake](#method-snake-case) -[Str::squish](#method-str-squish) -[Str::start](#method-str-start) -[Str::startsWith](#method-starts-with) -[Str::studly](#method-studly-case) -[Str::substr](#method-str-substr) -[Str::substrCount](#method-str-substrcount) -[Str::substrReplace](#method-str-substrreplace) -[Str::swap](#method-str-swap) -[Str::take](#method-take) -[Str::title](#method-title-case) -[Str::toBase64](#method-str-to-base64) -[Str::trim](#method-str-trim) -[Str::ltrim](#method-str-ltrim) -[Str::rtrim](#method-str-rtrim) -[Str::ucfirst](#method-str-ucfirst) -[Str::ucsplit](#method-str-ucsplit) -[Str::upper](#method-str-upper) -[Str::ulid](#method-str-ulid) -[Str::unwrap](#method-str-unwrap) -[Str::uuid](#method-str-uuid) -[Str::wordCount](#method-str-word-count) -[Str::wordWrap](#method-str-word-wrap) -[Str::words](#method-str-words) -[Str::wrap](#method-str-wrap) -[str](#method-str) -[trans](#method-trans) -[trans_choice](#method-trans-choice) - -
      - - -### Fluent Strings - -
      - -[after](#method-fluent-str-after) -[afterLast](#method-fluent-str-after-last) -[apa](#method-fluent-str-apa) -[append](#method-fluent-str-append) -[ascii](#method-fluent-str-ascii) -[basename](#method-fluent-str-basename) -[before](#method-fluent-str-before) -[beforeLast](#method-fluent-str-before-last) -[between](#method-fluent-str-between) -[betweenFirst](#method-fluent-str-between-first) -[camel](#method-fluent-str-camel) -[charAt](#method-fluent-str-char-at) -[classBasename](#method-fluent-str-class-basename) -[chopStart](#method-fluent-str-chop-start) -[chopEnd](#method-fluent-str-chop-end) -[contains](#method-fluent-str-contains) -[containsAll](#method-fluent-str-contains-all) -[dirname](#method-fluent-str-dirname) -[endsWith](#method-fluent-str-ends-with) -[exactly](#method-fluent-str-exactly) -[excerpt](#method-fluent-str-excerpt) -[explode](#method-fluent-str-explode) -[finish](#method-fluent-str-finish) -[headline](#method-fluent-str-headline) -[is](#method-fluent-str-is) -[isAscii](#method-fluent-str-is-ascii) -[isEmpty](#method-fluent-str-is-empty) -[isNotEmpty](#method-fluent-str-is-not-empty) -[isJson](#method-fluent-str-is-json) -[isUlid](#method-fluent-str-is-ulid) -[isUrl](#method-fluent-str-is-url) -[isUuid](#method-fluent-str-is-uuid) -[kebab](#method-fluent-str-kebab) -[lcfirst](#method-fluent-str-lcfirst) -[length](#method-fluent-str-length) -[limit](#method-fluent-str-limit) -[lower](#method-fluent-str-lower) -[mask](#method-fluent-str-mask) -[match](#method-fluent-str-match) -[matchAll](#method-fluent-str-match-all) -[isMatch](#method-fluent-str-is-match) -[newLine](#method-fluent-str-new-line) -[padBoth](#method-fluent-str-padboth) -[padLeft](#method-fluent-str-padleft) -[padRight](#method-fluent-str-padright) -[pipe](#method-fluent-str-pipe) -[plural](#method-fluent-str-plural) -[position](#method-fluent-str-position) -[prepend](#method-fluent-str-prepend) -[remove](#method-fluent-str-remove) -[repeat](#method-fluent-str-repeat) -[replace](#method-fluent-str-replace) -[replaceArray](#method-fluent-str-replace-array) -[replaceFirst](#method-fluent-str-replace-first) -[replaceLast](#method-fluent-str-replace-last) -[replaceMatches](#method-fluent-str-replace-matches) -[replaceStart](#method-fluent-str-replace-start) -[replaceEnd](#method-fluent-str-replace-end) -[scan](#method-fluent-str-scan) -[singular](#method-fluent-str-singular) -[slug](#method-fluent-str-slug) -[snake](#method-fluent-str-snake) -[split](#method-fluent-str-split) -[squish](#method-fluent-str-squish) -[start](#method-fluent-str-start) -[startsWith](#method-fluent-str-starts-with) -[stripTags](#method-fluent-str-strip-tags) -[studly](#method-fluent-str-studly) -[substr](#method-fluent-str-substr) -[substrReplace](#method-fluent-str-substrreplace) -[swap](#method-fluent-str-swap) -[take](#method-fluent-str-take) -[tap](#method-fluent-str-tap) -[test](#method-fluent-str-test) -[title](#method-fluent-str-title) -[toBase64](#method-fluent-str-to-base64) -[trim](#method-fluent-str-trim) -[ltrim](#method-fluent-str-ltrim) -[rtrim](#method-fluent-str-rtrim) -[ucfirst](#method-fluent-str-ucfirst) -[ucsplit](#method-fluent-str-ucsplit) -[unwrap](#method-fluent-str-unwrap) -[upper](#method-fluent-str-upper) -[when](#method-fluent-str-when) -[whenContains](#method-fluent-str-when-contains) -[whenContainsAll](#method-fluent-str-when-contains-all) -[whenEmpty](#method-fluent-str-when-empty) -[whenNotEmpty](#method-fluent-str-when-not-empty) -[whenStartsWith](#method-fluent-str-when-starts-with) -[whenEndsWith](#method-fluent-str-when-ends-with) -[whenExactly](#method-fluent-str-when-exactly) -[whenNotExactly](#method-fluent-str-when-not-exactly) -[whenIs](#method-fluent-str-when-is) -[whenIsAscii](#method-fluent-str-when-is-ascii) -[whenIsUlid](#method-fluent-str-when-is-ulid) -[whenIsUuid](#method-fluent-str-when-is-uuid) -[whenTest](#method-fluent-str-when-test) -[wordCount](#method-fluent-str-word-count) -[words](#method-fluent-str-words) -[wrap](#method-fluent-str-wrap) - -
      - - -## Strings - - -#### __() - -The `__` function translates the given translation string or translation key using your [language files](/docs/localization): - -```php -echo __('Welcome to our application'); - -echo __('messages.welcome'); -``` - -If the specified translation string or key does not exist, the `__` function will return the given value. So, using the example above, the `__` function would return `messages.welcome` if that translation key does not exist. - - -#### class_basename() - -The `class_basename` function returns the class name of the given class with the class's namespace removed: - -```php -$class = class_basename('Foo\Bar\Baz'); - -// Baz -``` - - -#### e() - -The `e` function runs PHP's `htmlspecialchars` function with the `double_encode` option set to `true` by default: - -```php -echo e('foo'); - -// <html>foo</html> -``` - - -#### preg_replace_array() - -The `preg_replace_array` function replaces a given pattern in the string sequentially using an array: - -```php -$string = 'The event will take place between :start and :end'; - -$replaced = preg_replace_array('/:[a-z_]+/', ['8:30', '9:00'], $string); - -// The event will take place between 8:30 and 9:00 -``` - - -#### Str::after() - -The `Str::after` method returns everything after the given value in a string. The entire string will be returned if the value does not exist within the string: - -```php -use Hypervel\Support\Str; - -$slice = Str::after('This is my name', 'This is'); - -// ' my name' -``` - - -#### Str::afterLast() - -The `Str::afterLast` method returns everything after the last occurrence of the given value in a string. The entire string will be returned if the value does not exist within the string: - -```php -use Hypervel\Support\Str; - -$slice = Str::afterLast('App\Http\Controllers\Controller', '\\'); - -// 'Controller' -``` - - -#### Str::apa() - -The `Str::apa` method converts the given string to title case following the [APA guidelines](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case): - -```php -use Hypervel\Support\Str; - -$title = Str::apa('Creating A Project'); - -// 'Creating a Project' -``` - - -#### Str::ascii() - -The `Str::ascii` method will attempt to transliterate the string into an ASCII value: - -```php -use Hypervel\Support\Str; - -$slice = Str::ascii('û'); - -// 'u' -``` - - -#### Str::before() - -The `Str::before` method returns everything before the given value in a string: - -```php -use Hypervel\Support\Str; - -$slice = Str::before('This is my name', 'my name'); - -// 'This is ' -``` - - -#### Str::beforeLast() - -The `Str::beforeLast` method returns everything before the last occurrence of the given value in a string: - -```php -use Hypervel\Support\Str; - -$slice = Str::beforeLast('This is my name', 'is'); - -// 'This ' -``` - - -#### Str::between() - -The `Str::between` method returns the portion of a string between two values: - -```php -use Hypervel\Support\Str; - -$slice = Str::between('This is my name', 'This', 'name'); - -// ' is my ' -``` - - -#### Str::betweenFirst() - -The `Str::betweenFirst` method returns the smallest possible portion of a string between two values: - -```php -use Hypervel\Support\Str; - -$slice = Str::betweenFirst('[a] bc [d]', '[', ']'); - -// 'a' -``` - - -#### Str::camel() - -The `Str::camel` method converts the given string to `camelCase`: - -```php -use Hypervel\Support\Str; - -$converted = Str::camel('foo_bar'); - -// 'fooBar' -``` - - -#### Str::charAt() - -The `Str::charAt` method returns the character at the specified index. If the index is out of bounds, `false` is returned: - -```php -use Hypervel\Support\Str; - -$character = Str::charAt('This is my name.', 6); - -// 's' -``` - - -#### Str::chopStart() - -The `Str::chopStart` method removes the first occurrence of the given value only if the value appears at the start of the string: - -```php -use Hypervel\Support\Str; - -$url = Str::chopStart('https://laravel.com', 'https://'); - -// 'laravel.com' -``` - -You may also pass an array as the second argument. If the string starts with any of the values in the array then that value will be removed from string: - -```php -use Hypervel\Support\Str; - -$url = Str::chopStart('http://laravel.com', ['https://', 'http://']); - -// 'laravel.com' -``` - - -#### Str::chopEnd() - -The `Str::chopEnd` method removes the last occurrence of the given value only if the value appears at the end of the string: - -```php -use Hypervel\Support\Str; - -$url = Str::chopEnd('app/Models/Photograph.php', '.php'); - -// 'app/Models/Photograph' -``` - -You may also pass an array as the second argument. If the string ends with any of the values in the array then that value will be removed from string: - -```php -use Hypervel\Support\Str; - -$url = Str::chopEnd('laravel.com/index.php', ['/index.html', '/index.php']); - -// 'laravel.com' -``` - - -#### Str::contains() - -The `Str::contains` method determines if the given string contains the given value. By default this method is case sensitive: - -```php -use Hypervel\Support\Str; - -$contains = Str::contains('This is my name', 'my'); - -// true -``` - -You may also pass an array of values to determine if the given string contains any of the values in the array: - -```php -use Hypervel\Support\Str; - -$contains = Str::contains('This is my name', ['my', 'foo']); - -// true -``` - -You may disable case sensitivity by setting the `ignoreCase` argument to `true`: - -```php -use Hypervel\Support\Str; - -$contains = Str::contains('This is my name', 'MY', ignoreCase: true); - -// true -``` - - -#### Str::containsAll() - -The `Str::containsAll` method determines if the given string contains all of the values in a given array: - -```php -use Hypervel\Support\Str; - -$containsAll = Str::containsAll('This is my name', ['my', 'name']); - -// true -``` - -You may disable case sensitivity by setting the `ignoreCase` argument to `true`: - -```php -use Hypervel\Support\Str; - -$containsAll = Str::containsAll('This is my name', ['MY', 'NAME'], ignoreCase: true); - -// true -``` - - -#### Str::doesntContain() - -The `Str::doesntContain` method determines if the given string doesn't contain the given value. By default this method is case sensitive: - -```php -use Hypervel\Support\Str; - -$doesntContain = Str::doesntContain('This is name', 'my'); - -// true -``` - -You may also pass an array of values to determine if the given string doesn't contain any of the values in the array: - -```php -use Hypervel\Support\Str; - -$doesntContain = Str::doesntContain('This is name', ['my', 'foo']); - -// true -``` - -You may disable case sensitivity by setting the `ignoreCase` argument to `true`: - -```php -use Hypervel\Support\Str; - -$doesntContain = Str::doesntContain('This is name', 'MY', ignoreCase: true); - -// true -``` - - -#### Str::endsWith() - -The `Str::endsWith` method determines if the given string ends with the given value: - -```php -use Hypervel\Support\Str; - -$result = Str::endsWith('This is my name', 'name'); - -// true -``` - -You may also pass an array of values to determine if the given string ends with any of the values in the array: - -```php -use Hypervel\Support\Str; - -$result = Str::endsWith('This is my name', ['name', 'foo']); - -// true - -$result = Str::endsWith('This is my name', ['this', 'foo']); - -// false -``` - - -#### Str::excerpt() - -The `Str::excerpt` method extracts an excerpt from a given string that matches the first instance of a phrase within that string: - -```php -use Hypervel\Support\Str; - -$excerpt = Str::excerpt('This is my name', 'my', [ - 'radius' => 3 -]); - -// '...is my na...' -``` - -The `radius` option, which defaults to `100`, allows you to define the number of characters that should appear on each side of the truncated string. - -In addition, you may use the `omission` option to define the string that will be prepended and appended to the truncated string: - -```php -use Hypervel\Support\Str; - -$excerpt = Str::excerpt('This is my name', 'name', [ - 'radius' => 3, - 'omission' => '(...) ' -]); - -// '(...) my name' -``` - - -#### Str::finish() - -The `Str::finish` method adds a single instance of the given value to a string if it does not already end with that value: - -```php -use Hypervel\Support\Str; - -$adjusted = Str::finish('this/string', '/'); - -// this/string/ - -$adjusted = Str::finish('this/string/', '/'); - -// this/string/ -``` - - -#### Str::headline() - -The `Str::headline` method will convert strings delimited by casing, hyphens, or underscores into a space delimited string with each word's first letter capitalized: - -```php -use Hypervel\Support\Str; - -$headline = Str::headline('steve_jobs'); - -// Steve Jobs - -$headline = Str::headline('EmailNotificationSent'); - -// Email Notification Sent -``` - - -#### Str::is() - -The `Str::is` method determines if a given string matches a given pattern. Asterisks may be used as wildcard values: - -```php -use Hypervel\Support\Str; - -$matches = Str::is('foo*', 'foobar'); - -// true - -$matches = Str::is('baz*', 'foobar'); - -// false -``` - -You may disable case sensitivity by setting the `ignoreCase` argument to `true`: - -```php -use Hypervel\Support\Str; - -$matches = Str::is('*.jpg', 'photo.JPG', ignoreCase: true); - -// true -``` - - -#### Str::isAscii() - -The `Str::isAscii` method determines if a given string is 7 bit ASCII: - -```php -use Hypervel\Support\Str; - -$isAscii = Str::isAscii('Taylor'); - -// true - -$isAscii = Str::isAscii('ü'); - -// false -``` - - -#### Str::isJson() - -The `Str::isJson` method determines if the given string is valid JSON: - -```php -use Hypervel\Support\Str; - -$result = Str::isJson('[1,2,3]'); - -// true - -$result = Str::isJson('{"first": "John", "last": "Doe"}'); - -// true - -$result = Str::isJson('{first: "John", last: "Doe"}'); - -// false -``` - - -#### Str::isUrl() - -The `Str::isUrl` method determines if the given string is a valid URL: - -```php -use Hypervel\Support\Str; - -$isUrl = Str::isUrl('http://example.com'); - -// true - -$isUrl = Str::isUrl('laravel'); - -// false -``` - -The `isUrl` method considers a wide range of protocols as valid. However, you may specify the protocols that should be considered valid by providing them to the `isUrl` method: - -```php -$isUrl = Str::isUrl('http://example.com', ['http', 'https']); -``` - - -#### Str::isUlid() - -The `Str::isUlid` method determines if the given string is a valid ULID: - -```php -use Hypervel\Support\Str; - -$isUlid = Str::isUlid('01gd6r360bp37zj17nxb55yv40'); - -// true - -$isUlid = Str::isUlid('laravel'); - -// false -``` - - -#### Str::isUuid() - -The `Str::isUuid` method determines if the given string is a valid UUID: - -```php -use Hypervel\Support\Str; - -$isUuid = Str::isUuid('a0a2a2d2-0b87-4a18-83f2-2529882be2de'); - -// true - -$isUuid = Str::isUuid('laravel'); - -// false -``` - - -#### Str::kebab() - -The `Str::kebab` method converts the given string to `kebab-case`: - -```php -use Hypervel\Support\Str; - -$converted = Str::kebab('fooBar'); - -// foo-bar -``` - - -#### Str::lcfirst() - -The `Str::lcfirst` method returns the given string with the first character lowercased: - -```php -use Hypervel\Support\Str; - -$string = Str::lcfirst('Foo Bar'); - -// foo Bar -``` - - -#### Str::length() - -The `Str::length` method returns the length of the given string: - -```php -use Hypervel\Support\Str; - -$length = Str::length('Laravel'); - -// 7 -``` - - -#### Str::limit() - -The `Str::limit` method truncates the given string to the specified length: - -```php -use Hypervel\Support\Str; - -$truncated = Str::limit('The quick brown fox jumps over the lazy dog', 20); - -// The quick brown fox... -``` - -You may pass a third argument to the method to change the string that will be appended to the end of the truncated string: - -```php -$truncated = Str::limit('The quick brown fox jumps over the lazy dog', 20, ' (...)'); - -// The quick brown fox (...) -``` - -If you would like to preserve complete words when truncating the string, you may utilize the `preserveWords` argument. When this argument is `true`, the string will be truncated to the nearest complete word boundary: - -```php -$truncated = Str::limit('The quick brown fox', 12, preserveWords: true); - -// The quick... -``` - - -#### Str::lower() - -The `Str::lower` method converts the given string to lowercase: - -```php -use Hypervel\Support\Str; - -$converted = Str::lower('LARAVEL'); - -// laravel -``` - - -#### Str::mask() - -The `Str::mask` method masks a portion of a string with a repeated character, and may be used to obfuscate segments of strings such as email addresses and phone numbers: - -```php -use Hypervel\Support\Str; - -$string = Str::mask('taylor@example.com', '*', 3); - -// tay*************** -``` - -If needed, you provide a negative number as the third argument to the `mask` method, which will instruct the method to begin masking at the given distance from the end of the string: - -```php -$string = Str::mask('taylor@example.com', '*', -15, 3); - -// tay***@example.com -``` - - -#### Str::orderedUuid() - -The `Str::orderedUuid` method generates a "timestamp first" UUID that may be efficiently stored in an indexed database column. Each UUID that is generated using this method will be sorted after UUIDs previously generated using the method: - -```php -use Hypervel\Support\Str; - -return (string) Str::orderedUuid(); -``` - - -#### Str::padBoth() - -The `Str::padBoth` method wraps PHP's `str_pad` function, padding both sides of a string with another string until the final string reaches a desired length: - -```php -use Hypervel\Support\Str; - -$padded = Str::padBoth('James', 10, '_'); - -// '__James___' - -$padded = Str::padBoth('James', 10); - -// ' James ' -``` - - -#### Str::padLeft() - -The `Str::padLeft` method wraps PHP's `str_pad` function, padding the left side of a string with another string until the final string reaches a desired length: - -```php -use Hypervel\Support\Str; - -$padded = Str::padLeft('James', 10, '-='); - -// '-=-=-James' - -$padded = Str::padLeft('James', 10); - -// ' James' -``` - - -#### Str::padRight() - -The `Str::padRight` method wraps PHP's `str_pad` function, padding the right side of a string with another string until the final string reaches a desired length: - -```php -use Hypervel\Support\Str; - -$padded = Str::padRight('James', 10, '-'); - -// 'James-----' - -$padded = Str::padRight('James', 10); - -// 'James ' -``` - - -#### Str::password() - -The `Str::password` method may be used to generate a secure, random password of a given length. The password will consist of a combination of letters, numbers, symbols, and spaces. By default, passwords are 32 characters long: - -```php -use Hypervel\Support\Str; - -$password = Str::password(); - -// 'EbJo2vE-AS:U,$%_gkrV4n,q~1xy/-_4' - -$password = Str::password(12); - -// 'qwuar>#V|i]N' -``` - - -#### Str::plural() - -The `Str::plural` method converts a singular word string to its plural form. This function supports [any of the languages support by Laravel's pluralizer](/docs/localization#pluralization-language): - -```php -use Hypervel\Support\Str; - -$plural = Str::plural('car'); - -// cars - -$plural = Str::plural('child'); - -// children -``` - -You may provide an integer as a second argument to the function to retrieve the singular or plural form of the string: - -```php -use Hypervel\Support\Str; - -$plural = Str::plural('child', 2); - -// children - -$singular = Str::plural('child', 1); - -// child -``` - - -#### Str::pluralStudly() - -The `Str::pluralStudly` method converts a singular word string formatted in studly caps case to its plural form. This function supports [any of the languages support by Laravel's pluralizer](/docs/localization#pluralization-language): - -```php -use Hypervel\Support\Str; - -$plural = Str::pluralStudly('VerifiedHuman'); - -// VerifiedHumans - -$plural = Str::pluralStudly('UserFeedback'); - -// UserFeedback -``` - -You may provide an integer as a second argument to the function to retrieve the singular or plural form of the string: - -```php -use Hypervel\Support\Str; - -$plural = Str::pluralStudly('VerifiedHuman', 2); - -// VerifiedHumans - -$singular = Str::pluralStudly('VerifiedHuman', 1); - -// VerifiedHuman -``` - - -#### Str::position() - -The `Str::position` method returns the position of the first occurrence of a substring in a string. If the substring does not exist in the given string, `false` is returned: - -```php -use Hypervel\Support\Str; - -$position = Str::position('Hello, World!', 'Hello'); - -// 0 - -$position = Str::position('Hello, World!', 'W'); - -// 7 -``` - - -#### Str::random() - -The `Str::random` method generates a random string of the specified length. This function uses PHP's `random_bytes` function: - -```php -use Hypervel\Support\Str; - -$random = Str::random(40); -``` - -During testing, it may be useful to "fake" the value that is returned by the `Str::random` method. To accomplish this, you may use the `createRandomStringsUsing` method: - -```php -Str::createRandomStringsUsing(function () { - return 'fake-random-string'; -}); -``` - -To instruct the `random` method to return to generating random strings normally, you may invoke the `createRandomStringsNormally` method: - -```php -Str::createRandomStringsNormally(); -``` - - -#### Str::remove() - -The `Str::remove` method removes the given value or array of values from the string: - -```php -use Hypervel\Support\Str; - -$string = 'Peter Piper picked a peck of pickled peppers.'; - -$removed = Str::remove('e', $string); - -// Ptr Pipr pickd a pck of pickld ppprs. -``` - -You may also pass `false` as a third argument to the `remove` method to ignore case when removing strings. - - -#### Str::repeat() - -The `Str::repeat` method repeats the given string: - -```php -use Hypervel\Support\Str; - -$string = 'a'; - -$repeat = Str::repeat($string, 5); - -// aaaaa -``` - - -#### Str::replace() - -The `Str::replace` method replaces a given string within the string: - -```php -use Hypervel\Support\Str; - -$string = 'Laravel 11.x'; - -$replaced = Str::replace('11.x', '12.x', $string); - -// Laravel 12.x -``` - -The `replace` method also accepts a `caseSensitive` argument. By default, the `replace` method is case sensitive: - -```php -Str::replace('Framework', 'Laravel', caseSensitive: false); -``` - - -#### Str::replaceArray() - -The `Str::replaceArray` method replaces a given value in the string sequentially using an array: - -```php -use Hypervel\Support\Str; - -$string = 'The event will take place between ? and ?'; - -$replaced = Str::replaceArray('?', ['8:30', '9:00'], $string); - -// The event will take place between 8:30 and 9:00 -``` - - -#### Str::replaceFirst() - -The `Str::replaceFirst` method replaces the first occurrence of a given value in a string: - -```php -use Hypervel\Support\Str; - -$replaced = Str::replaceFirst('the', 'a', 'the quick brown fox jumps over the lazy dog'); - -// a quick brown fox jumps over the lazy dog -``` - - -#### Str::replaceLast() - -The `Str::replaceLast` method replaces the last occurrence of a given value in a string: - -```php -use Hypervel\Support\Str; - -$replaced = Str::replaceLast('the', 'a', 'the quick brown fox jumps over the lazy dog'); - -// the quick brown fox jumps over a lazy dog -``` - - -#### Str::replaceMatches() - -The `Str::replaceMatches` method replaces all portions of a string matching a pattern with the given replacement string: - -```php -use Hypervel\Support\Str; - -$replaced = Str::replaceMatches( - pattern: '/[^A-Za-z0-9]++/', - replace: '', - subject: '(+1) 501-555-1000' -) - -// '15015551000' -``` - -The `replaceMatches` method also accepts a closure that will be invoked with each portion of the string matching the given pattern, allowing you to perform the replacement logic within the closure and return the replaced value: - -```php -use Hypervel\Support\Str; - -$replaced = Str::replaceMatches('/\d/', function (array $matches) { - return '['.$matches[0].']'; -}, '123'); - -// '[1][2][3]' -``` - - -#### Str::replaceStart() - -The `Str::replaceStart` method replaces the first occurrence of the given value only if the value appears at the start of the string: - -```php -use Hypervel\Support\Str; - -$replaced = Str::replaceStart('Hello', 'Laravel', 'Hello World'); - -// Laravel World - -$replaced = Str::replaceStart('World', 'Laravel', 'Hello World'); - -// Hello World -``` - - -#### Str::replaceEnd() - -The `Str::replaceEnd` method replaces the last occurrence of the given value only if the value appears at the end of the string: - -```php -use Hypervel\Support\Str; - -$replaced = Str::replaceEnd('World', 'Laravel', 'Hello World'); - -// Hello Laravel - -$replaced = Str::replaceEnd('Hello', 'Laravel', 'Hello World'); - -// Hello World -``` - - -#### Str::reverse() - -The `Str::reverse` method reverses the given string: - -```php -use Hypervel\Support\Str; - -$reversed = Str::reverse('Hello World'); - -// dlroW olleH -``` - - -#### Str::singular() - -The `Str::singular` method converts a string to its singular form. This function supports [any of the languages support by Laravel's pluralizer](/docs/localization#pluralization-language): - -```php -use Hypervel\Support\Str; - -$singular = Str::singular('cars'); - -// car - -$singular = Str::singular('children'); - -// child -``` - - -#### Str::slug() - -The `Str::slug` method generates a URL friendly "slug" from the given string: - -```php -use Hypervel\Support\Str; - -$slug = Str::slug('Laravel 5 Framework', '-'); - -// laravel-5-framework -``` - - -#### Str::snake() - -The `Str::snake` method converts the given string to `snake_case`: - -```php -use Hypervel\Support\Str; - -$converted = Str::snake('fooBar'); - -// foo_bar - -$converted = Str::snake('fooBar', '-'); - -// foo-bar -``` - - -#### Str::squish() - -The `Str::squish` method removes all extraneous white space from a string, including extraneous white space between words: - -```php -use Hypervel\Support\Str; - -$string = Str::squish(' Hypervel framework '); - -// Hypervel framework -``` - - -#### Str::start() - -The `Str::start` method adds a single instance of the given value to a string if it does not already start with that value: - -```php -use Hypervel\Support\Str; - -$adjusted = Str::start('this/string', '/'); - -// /this/string - -$adjusted = Str::start('/this/string', '/'); - -// /this/string -``` - - -#### Str::startsWith() - -The `Str::startsWith` method determines if the given string begins with the given value: - -```php -use Hypervel\Support\Str; - -$result = Str::startsWith('This is my name', 'This'); - -// true -``` - -If an array of possible values is passed, the `startsWith` method will return `true` if the string begins with any of the given values: - -```php -$result = Str::startsWith('This is my name', ['This', 'That', 'There']); - -// true -``` - - -#### Str::studly() - -The `Str::studly` method converts the given string to `StudlyCase`: - -```php -use Hypervel\Support\Str; - -$converted = Str::studly('foo_bar'); - -// FooBar -``` - - -#### Str::substr() - -The `Str::substr` method returns the portion of string specified by the start and length parameters: - -```php -use Hypervel\Support\Str; - -$converted = Str::substr('The Laravel Framework', 4, 7); - -// Laravel -``` - - -#### Str::substrCount() - -The `Str::substrCount` method returns the number of occurrences of a given value in the given string: - -```php -use Hypervel\Support\Str; - -$count = Str::substrCount('If you like ice cream, you will like snow cones.', 'like'); - -// 2 -``` - - -#### Str::substrReplace() - -The `Str::substrReplace` method replaces text within a portion of a string, starting at the position specified by the third argument and replacing the number of characters specified by the fourth argument. Passing `0` to the method's fourth argument will insert the string at the specified position without replacing any of the existing characters in the string: - -```php -use Hypervel\Support\Str; - -$result = Str::substrReplace('1300', ':', 2); -// 13: - -$result = Str::substrReplace('1300', ':', 2, 0); -// 13:00 -``` - - -#### Str::swap() - -The `Str::swap` method replaces multiple values in the given string using PHP's `strtr` function: - -```php -use Hypervel\Support\Str; - -$string = Str::swap([ - 'Tacos' => 'Burritos', - 'great' => 'fantastic', -], 'Tacos are great!'); - -// Burritos are fantastic! -``` - - -#### Str::take() - -The `Str::take` method returns a specified number of characters from the beginning of a string: - -```php -use Hypervel\Support\Str; - -$taken = Str::take('Build something amazing!', 5); - -// Build -``` - - -#### Str::title() - -The `Str::title` method converts the given string to `Title Case`: - -```php -use Hypervel\Support\Str; - -$converted = Str::title('a nice title uses the correct case'); - -// A Nice Title Uses The Correct Case -``` - - -#### Str::toBase64() - -The `Str::toBase64` method converts the given string to Base64: - -```php -use Hypervel\Support\Str; - -$base64 = Str::toBase64('Laravel'); - -// TGFyYXZlbA== -``` - - -#### Str::trim() - -The `Str::trim` method strips whitespace (or other characters) from the beginning and end of the given string. Unlike PHP's native `trim` function, the `Str::trim` method also removes unicode whitespace characters: - -```php -use Hypervel\Support\Str; - -$string = Str::trim(' foo bar '); - -// 'foo bar' -``` - - -#### Str::ltrim() - -The `Str::ltrim` method strips whitespace (or other characters) from the beginning of the given string. Unlike PHP's native `ltrim` function, the `Str::ltrim` method also removes unicode whitespace characters: - -```php -use Hypervel\Support\Str; - -$string = Str::ltrim(' foo bar '); - -// 'foo bar ' -``` - - -#### Str::rtrim() - -The `Str::rtrim` method strips whitespace (or other characters) from the end of the given string. Unlike PHP's native `rtrim` function, the `Str::rtrim` method also removes unicode whitespace characters: - -```php -use Hypervel\Support\Str; - -$string = Str::rtrim(' foo bar '); - -// ' foo bar' -``` - - -#### Str::ucfirst() - -The `Str::ucfirst` method returns the given string with the first character capitalized: - -```php -use Hypervel\Support\Str; - -$string = Str::ucfirst('foo bar'); - -// Foo bar -``` - - -#### Str::ucsplit() - -The `Str::ucsplit` method splits the given string into an array by uppercase characters: - -```php -use Hypervel\Support\Str; - -$segments = Str::ucsplit('FooBar'); - -// [0 => 'Foo', 1 => 'Bar'] -``` - - -#### Str::upper() - -The `Str::upper` method converts the given string to uppercase: - -```php -use Hypervel\Support\Str; - -$string = Str::upper('laravel'); - -// LARAVEL -``` - - -#### Str::ulid() - -The `Str::ulid` method generates a ULID, which is a compact, time-ordered unique identifier: - -```php -use Hypervel\Support\Str; - -return (string) Str::ulid(); - -// 01gd6r360bp37zj17nxb55yv40 -``` - -If you would like to retrieve a `Hypervel\Support\Carbon` date instance representing the date and time that a given ULID was created, you may use the `createFromId` method provided by Laravel's Carbon integration: - -```php -use Hypervel\Support\Carbon; -use Hypervel\Support\Str; - -$date = Carbon::createFromId((string) Str::ulid()); -``` - -During testing, it may be useful to "fake" the value that is returned by the `Str::ulid` method. To accomplish this, you may use the `createUlidsUsing` method: - -```php -use Symfony\Component\Uid\Ulid; - -Str::createUlidsUsing(function () { - return new Ulid('01HRDBNHHCKNW2AK4Z29SN82T9'); -}); -``` - -To instruct the `ulid` method to return to generating ULIDs normally, you may invoke the `createUlidsNormally` method: - -```php -Str::createUlidsNormally(); -``` - - -#### Str::unwrap() - -The `Str::unwrap` method removes the specified strings from the beginning and end of a given string: - -```php -use Hypervel\Support\Str; - -Str::unwrap('-Laravel-', '-'); - -// Laravel - -Str::unwrap('{framework: "Laravel"}', '{', '}'); - -// framework: "Laravel" -``` - - -#### Str::uuid() - -The `Str::uuid` method generates a UUID (version 4): - -```php -use Hypervel\Support\Str; - -return (string) Str::uuid(); -``` - -During testing, it may be useful to "fake" the value that is returned by the `Str::uuid` method. To accomplish this, you may use the `createUuidsUsing` method: - -```php -use Ramsey\Uuid\Uuid; - -Str::createUuidsUsing(function () { - return Uuid::fromString('eadbfeac-5258-45c2-bab7-ccb9b5ef74f9'); -}); -``` - -To instruct the `uuid` method to return to generating UUIDs normally, you may invoke the `createUuidsNormally` method: - -```php -Str::createUuidsNormally(); -``` - - -#### Str::wordCount() - -The `Str::wordCount` method returns the number of words that a string contains: - -```php -use Hypervel\Support\Str; - -Str::wordCount('Hello, world!'); // 2 -``` - - -#### Str::wordWrap() - -The `Str::wordWrap` method wraps a string to a given number of characters: - -```php -use Hypervel\Support\Str; - -$text = "The quick brown fox jumped over the lazy dog." - -Str::wordWrap($text, characters: 20, break: "
      \n"); - -/* -The quick brown fox
      -jumped over the lazy
      -dog. -*/ -``` - - -#### Str::words() - -The `Str::words` method limits the number of words in a string. An additional string may be passed to this method via its third argument to specify which string should be appended to the end of the truncated string: - -```php -use Hypervel\Support\Str; - -return Str::words('Perfectly balanced, as all things should be.', 3, ' >>>'); - -// Perfectly balanced, as >>> -``` - - -#### Str::wrap() - -The `Str::wrap` method wraps the given string with an additional string or pair of strings: - -```php -use Hypervel\Support\Str; - -Str::wrap('Laravel', '"'); - -// "Laravel" - -Str::wrap('is', before: 'This ', after: ' Laravel!'); - -// This is Laravel! -``` - - -#### str() - -The `str` function returns a new `Hypervel\Support\Stringable` instance of the given string. This function is equivalent to the `Str::of` method: - -```php -$string = str('Taylor')->append(' Otwell'); - -// 'Taylor Otwell' -``` - -If no argument is provided to the `str` function, the function returns an instance of `Hypervel\Support\Str`: - -```php -$snake = str()->snake('FooBar'); - -// 'foo_bar' -``` - - -#### trans() - -The `trans` function translates the given translation key using your [language files](/docs/localization): - -```php -echo trans('messages.welcome'); -``` - -If the specified translation key does not exist, the `trans` function will return the given key. So, using the example above, the `trans` function would return `messages.welcome` if the translation key does not exist. - - -#### trans_choice() - -The `trans_choice` function translates the given translation key with inflection: - -```php -echo trans_choice('messages.notifications', $unreadCount); -``` - -If the specified translation key does not exist, the `trans_choice` function will return the given key. So, using the example above, the `trans_choice` function would return `messages.notifications` if the translation key does not exist. - - -## Fluent Strings - -Fluent strings provide a more fluent, object-oriented interface for working with string values, allowing you to chain multiple string operations together using a more readable syntax compared to traditional string operations. - - -#### after - -The `after` method returns everything after the given value in a string. The entire string will be returned if the value does not exist within the string: - -```php -use Hypervel\Support\Str; - -$slice = Str::of('This is my name')->after('This is'); - -// ' my name' -``` - - -#### afterLast - -The `afterLast` method returns everything after the last occurrence of the given value in a string. The entire string will be returned if the value does not exist within the string: - -```php -use Hypervel\Support\Str; - -$slice = Str::of('App\Http\Controllers\Controller')->afterLast('\\'); - -// 'Controller' -``` - - -#### apa - -The `apa` method converts the given string to title case following the [APA guidelines](https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case): - -```php -use Hypervel\Support\Str; - -$converted = Str::of('a nice title uses the correct case')->apa(); - -// A Nice Title Uses the Correct Case -``` - - -#### append - -The `append` method appends the given values to the string: - -```php -use Hypervel\Support\Str; - -$string = Str::of('Taylor')->append(' Otwell'); - -// 'Taylor Otwell' -``` - - -#### ascii - -The `ascii` method will attempt to transliterate the string into an ASCII value: - -```php -use Hypervel\Support\Str; - -$string = Str::of('ü')->ascii(); - -// 'u' -``` - - -#### basename - -The `basename` method will return the trailing name component of the given string: - -```php -use Hypervel\Support\Str; - -$string = Str::of('/foo/bar/baz')->basename(); - -// 'baz' -``` - -If needed, you may provide an "extension" that will be removed from the trailing component: - -```php -use Hypervel\Support\Str; - -$string = Str::of('/foo/bar/baz.jpg')->basename('.jpg'); - -// 'baz' -``` - - -#### before - -The `before` method returns everything before the given value in a string: - -```php -use Hypervel\Support\Str; - -$slice = Str::of('This is my name')->before('my name'); - -// 'This is ' -``` - - -#### beforeLast - -The `beforeLast` method returns everything before the last occurrence of the given value in a string: - -```php -use Hypervel\Support\Str; - -$slice = Str::of('This is my name')->beforeLast('is'); - -// 'This ' -``` - - -#### between - -The `between` method returns the portion of a string between two values: - -```php -use Hypervel\Support\Str; - -$converted = Str::of('This is my name')->between('This', 'name'); - -// ' is my ' -``` - - -#### betweenFirst - -The `betweenFirst` method returns the smallest possible portion of a string between two values: - -```php -use Hypervel\Support\Str; - -$converted = Str::of('[a] bc [d]')->betweenFirst('[', ']'); - -// 'a' -``` - - -#### camel - -The `camel` method converts the given string to `camelCase`: - -```php -use Hypervel\Support\Str; - -$converted = Str::of('foo_bar')->camel(); - -// 'fooBar' -``` - - -#### charAt - -The `charAt` method returns the character at the specified index. If the index is out of bounds, `false` is returned: - -```php -use Hypervel\Support\Str; - -$character = Str::of('This is my name.')->charAt(6); - -// 's' -``` - - -#### classBasename - -The `classBasename` method returns the class name of the given class with the class's namespace removed: - -```php -use Hypervel\Support\Str; - -$class = Str::of('Foo\Bar\Baz')->classBasename(); - -// 'Baz' -``` - - -#### chopStart - -The `chopStart` method removes the first occurrence of the given value only if the value appears at the start of the string: - -```php -use Hypervel\Support\Str; - -$url = Str::of('https://laravel.com')->chopStart('https://'); - -// 'laravel.com' -``` - -You may also pass an array. If the string starts with any of the values in the array then that value will be removed from string: - -```php -use Hypervel\Support\Str; - -$url = Str::of('http://laravel.com')->chopStart(['https://', 'http://']); - -// 'laravel.com' -``` - - -#### chopEnd - -The `chopEnd` method removes the last occurrence of the given value only if the value appears at the end of the string: - -```php -use Hypervel\Support\Str; - -$url = Str::of('https://laravel.com')->chopEnd('.com'); - -// 'https://laravel' -``` - -You may also pass an array. If the string ends with any of the values in the array then that value will be removed from string: - -```php -use Hypervel\Support\Str; - -$url = Str::of('http://laravel.com')->chopEnd(['.com', '.io']); - -// 'http://laravel' -``` - - -#### contains - -The `contains` method determines if the given string contains the given value. By default this method is case sensitive: - -```php -use Hypervel\Support\Str; - -$contains = Str::of('This is my name')->contains('my'); - -// true -``` - -You may also pass an array of values to determine if the given string contains any of the values in the array: - -```php -use Hypervel\Support\Str; - -$contains = Str::of('This is my name')->contains(['my', 'foo']); - -// true -``` - -You can disable case sensitivity by setting the `ignoreCase` argument to `true`: - -```php -use Hypervel\Support\Str; - -$contains = Str::of('This is my name')->contains('MY', ignoreCase: true); - -// true -``` - - -#### containsAll - -The `containsAll` method determines if the given string contains all of the values in the given array: - -```php -use Hypervel\Support\Str; - -$containsAll = Str::of('This is my name')->containsAll(['my', 'name']); - -// true -``` - -You can disable case sensitivity by setting the `ignoreCase` argument to `true`: - -```php -use Hypervel\Support\Str; - -$containsAll = Str::of('This is my name')->containsAll(['MY', 'NAME'], ignoreCase: true); - -// true -``` - - -#### dirname - -The `dirname` method returns the parent directory portion of the given string: - -```php -use Hypervel\Support\Str; - -$string = Str::of('/foo/bar/baz')->dirname(); - -// '/foo/bar' -``` - -If necessary, you may specify how many directory levels you wish to trim from the string: - -```php -use Hypervel\Support\Str; - -$string = Str::of('/foo/bar/baz')->dirname(2); - -// '/foo' -``` - - -#### endsWith - -The `endsWith` method determines if the given string ends with the given value: - -```php -use Hypervel\Support\Str; - -$result = Str::of('This is my name')->endsWith('name'); - -// true -``` - -You may also pass an array of values to determine if the given string ends with any of the values in the array: - -```php -use Hypervel\Support\Str; - -$result = Str::of('This is my name')->endsWith(['name', 'foo']); - -// true - -$result = Str::of('This is my name')->endsWith(['this', 'foo']); - -// false -``` - - -#### exactly - -The `exactly` method determines if the given string is an exact match with another string: - -```php -use Hypervel\Support\Str; - -$result = Str::of('Laravel')->exactly('Laravel'); - -// true -``` - - -#### excerpt - -The `excerpt` method extracts an excerpt from the string that matches the first instance of a phrase within that string: - -```php -use Hypervel\Support\Str; - -$excerpt = Str::of('This is my name')->excerpt('my', [ - 'radius' => 3 -]); - -// '...is my na...' -``` - -The `radius` option, which defaults to `100`, allows you to define the number of characters that should appear on each side of the truncated string. - -In addition, you may use the `omission` option to change the string that will be prepended and appended to the truncated string: - -```php -use Hypervel\Support\Str; - -$excerpt = Str::of('This is my name')->excerpt('name', [ - 'radius' => 3, - 'omission' => '(...) ' -]); - -// '(...) my name' -``` - - -#### explode - -The `explode` method splits the string by the given delimiter and returns a collection containing each section of the split string: - -```php -use Hypervel\Support\Str; - -$collection = Str::of('foo bar baz')->explode(' '); - -// collect(['foo', 'bar', 'baz']) -``` - - -#### finish - -The `finish` method adds a single instance of the given value to a string if it does not already end with that value: - -```php -use Hypervel\Support\Str; - -$adjusted = Str::of('this/string')->finish('/'); - -// this/string/ - -$adjusted = Str::of('this/string/')->finish('/'); - -// this/string/ -``` - - -#### headline - -The `headline` method will convert strings delimited by casing, hyphens, or underscores into a space delimited string with each word's first letter capitalized: - -```php -use Hypervel\Support\Str; - -$headline = Str::of('taylor_otwell')->headline(); - -// Taylor Otwell - -$headline = Str::of('EmailNotificationSent')->headline(); - -// Email Notification Sent -``` - - -#### is - -The `is` method determines if a given string matches a given pattern. Asterisks may be used as wildcard values - -```php -use Hypervel\Support\Str; - -$matches = Str::of('foobar')->is('foo*'); - -// true - -$matches = Str::of('foobar')->is('baz*'); - -// false -``` - - -#### isAscii - -The `isAscii` method determines if a given string is an ASCII string: - -```php -use Hypervel\Support\Str; - -$result = Str::of('Taylor')->isAscii(); - -// true - -$result = Str::of('ü')->isAscii(); - -// false -``` - - -#### isEmpty - -The `isEmpty` method determines if the given string is empty: - -```php -use Hypervel\Support\Str; - -$result = Str::of(' ')->trim()->isEmpty(); - -// true - -$result = Str::of('Laravel')->trim()->isEmpty(); - -// false -``` - - -#### isNotEmpty - -The `isNotEmpty` method determines if the given string is not empty: - -```php -use Hypervel\Support\Str; - -$result = Str::of(' ')->trim()->isNotEmpty(); - -// false - -$result = Str::of('Laravel')->trim()->isNotEmpty(); - -// true -``` - - -#### isJson - -The `isJson` method determines if a given string is valid JSON: - -```php -use Hypervel\Support\Str; - -$result = Str::of('[1,2,3]')->isJson(); - -// true - -$result = Str::of('{"first": "John", "last": "Doe"}')->isJson(); - -// true - -$result = Str::of('{first: "John", last: "Doe"}')->isJson(); - -// false -``` - - -#### isUlid - -The `isUlid` method determines if a given string is a ULID: - -```php -use Hypervel\Support\Str; - -$result = Str::of('01gd6r360bp37zj17nxb55yv40')->isUlid(); - -// true - -$result = Str::of('Taylor')->isUlid(); - -// false -``` - - -#### isUrl - -The `isUrl` method determines if a given string is a URL: - -```php -use Hypervel\Support\Str; - -$result = Str::of('http://example.com')->isUrl(); - -// true - -$result = Str::of('Taylor')->isUrl(); - -// false -``` - -The `isUrl` method considers a wide range of protocols as valid. However, you may specify the protocols that should be considered valid by providing them to the `isUrl` method: - -```php -$result = Str::of('http://example.com')->isUrl(['http', 'https']); -``` - - -#### isUuid - -The `isUuid` method determines if a given string is a UUID: - -```php -use Hypervel\Support\Str; - -$result = Str::of('5ace9ab9-e9cf-4ec6-a19d-5881212a452c')->isUuid(); - -// true - -$result = Str::of('Taylor')->isUuid(); - -// false -``` - - -#### kebab - -The `kebab` method converts the given string to `kebab-case`: - -```php -use Hypervel\Support\Str; - -$converted = Str::of('fooBar')->kebab(); - -// foo-bar -``` - - -#### lcfirst - -The `lcfirst` method returns the given string with the first character lowercased: - -```php -use Hypervel\Support\Str; - -$string = Str::of('Foo Bar')->lcfirst(); - -// foo Bar -``` - - -#### length - -The `length` method returns the length of the given string: - -```php -use Hypervel\Support\Str; - -$length = Str::of('Laravel')->length(); - -// 7 -``` - - -#### limit - -The `limit` method truncates the given string to the specified length: - -```php -use Hypervel\Support\Str; - -$truncated = Str::of('The quick brown fox jumps over the lazy dog')->limit(20); - -// The quick brown fox... -``` - -You may also pass a second argument to change the string that will be appended to the end of the truncated string: - -```php -$truncated = Str::of('The quick brown fox jumps over the lazy dog')->limit(20, ' (...)'); - -// The quick brown fox (...) -``` - -If you would like to preserve complete words when truncating the string, you may utilize the `preserveWords` argument. When this argument is `true`, the string will be truncated to the nearest complete word boundary: - -```php -$truncated = Str::of('The quick brown fox')->limit(12, preserveWords: true); - -// The quick... -``` - - -#### lower - -The `lower` method converts the given string to lowercase: - -```php -use Hypervel\Support\Str; - -$result = Str::of('LARAVEL')->lower(); - -// 'laravel' -``` - - -#### markdown - -The `markdown` method converts GitHub flavored Markdown into HTML: - -```php -use Hypervel\Support\Str; - -$html = Str::of('# Laravel')->markdown(); - -//

      Laravel

      - -$html = Str::of('# Taylor Otwell')->markdown([ - 'html_input' => 'strip', -]); - -//

      Taylor Otwell

      -``` - -#### Markdown Security - -By default, Markdown supports raw HTML, which will expose Cross-Site Scripting (XSS) vulnerabilities when used with raw user input. As per the [CommonMark Security documentation](https://commonmark.thephpleague.com/security/), you may use the `html_input` option to either escape or strip raw HTML, and the `allow_unsafe_links` option to specify whether to allow unsafe links. If you need to allow some raw HTML, you should pass your compiled Markdown through an HTML Purifier: - -```php -use Hypervel\Support\Str; - -Str::of('Inject: ')->markdown([ - 'html_input' => 'strip', - 'allow_unsafe_links' => false, -]); - -//

      Inject: alert("Hello XSS!");

      -``` - - -#### mask - -The `mask` method masks a portion of a string with a repeated character, and may be used to obfuscate segments of strings such as email addresses and phone numbers: - -```php -use Hypervel\Support\Str; - -$string = Str::of('taylor@example.com')->mask('*', 3); - -// tay*************** -``` - -If needed, you may provide negative numbers as the third or fourth argument to the `mask` method, which will instruct the method to begin masking at the given distance from the end of the string: - -```php -$string = Str::of('taylor@example.com')->mask('*', -15, 3); - -// tay***@example.com - -$string = Str::of('taylor@example.com')->mask('*', 4, -4); - -// tayl**********.com -``` - - -#### match - -The `match` method will return the portion of a string that matches a given regular expression pattern: - -```php -use Hypervel\Support\Str; - -$result = Str::of('foo bar')->match('/bar/'); - -// 'bar' - -$result = Str::of('foo bar')->match('/foo (.*)/'); - -// 'bar' -``` - - -#### matchAll - -The `matchAll` method will return a collection containing the portions of a string that match a given regular expression pattern: - -```php -use Hypervel\Support\Str; - -$result = Str::of('bar foo bar')->matchAll('/bar/'); - -// collect(['bar', 'bar']) -``` - -If you specify a matching group within the expression, Hypervel will return a collection of the first matching group's matches: - -```php -use Hypervel\Support\Str; - -$result = Str::of('bar fun bar fly')->matchAll('/f(\w*)/'); - -// collect(['un', 'ly']); -``` - -If no matches are found, an empty collection will be returned. - - -#### isMatch - -The `isMatch` method will return `true` if the string matches a given regular expression: - -```php -use Hypervel\Support\Str; - -$result = Str::of('foo bar')->isMatch('/foo (.*)/'); - -// true - -$result = Str::of('laravel')->isMatch('/foo (.*)/'); - -// false -``` - - -#### newLine - -The `newLine` method appends an "end of line" character to a string: - -```php -use Hypervel\Support\Str; - -$padded = Str::of('Laravel')->newLine()->append('Framework'); - -// 'Laravel -// Framework' -``` - - -#### padBoth - -The `padBoth` method wraps PHP's `str_pad` function, padding both sides of a string with another string until the final string reaches the desired length: - -```php -use Hypervel\Support\Str; - -$padded = Str::of('James')->padBoth(10, '_'); - -// '__James___' - -$padded = Str::of('James')->padBoth(10); - -// ' James ' -``` - - -#### padLeft - -The `padLeft` method wraps PHP's `str_pad` function, padding the left side of a string with another string until the final string reaches the desired length: - -```php -use Hypervel\Support\Str; - -$padded = Str::of('James')->padLeft(10, '-='); - -// '-=-=-James' - -$padded = Str::of('James')->padLeft(10); - -// ' James' -``` - - -#### padRight - -The `padRight` method wraps PHP's `str_pad` function, padding the right side of a string with another string until the final string reaches the desired length: - -```php -use Hypervel\Support\Str; - -$padded = Str::of('James')->padRight(10, '-'); - -// 'James-----' - -$padded = Str::of('James')->padRight(10); - -// 'James ' -``` - - -#### pipe - -The `pipe` method allows you to transform the string by passing its current value to the given callable: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$hash = Str::of('Laravel')->pipe('md5')->prepend('Checksum: '); - -// 'Checksum: a5c95b86291ea299fcbe64458ed12702' - -$closure = Str::of('foo')->pipe(function (Stringable $str) { - return 'bar'; -}); - -// 'bar' -``` - - -#### plural - -The `plural` method converts a singular word string to its plural form. This function supports [any of the languages support by Laravel's pluralizer](/docs/localization#pluralization-language): - -```php -use Hypervel\Support\Str; - -$plural = Str::of('car')->plural(); - -// cars - -$plural = Str::of('child')->plural(); - -// children -``` - -You may provide an integer as a second argument to the function to retrieve the singular or plural form of the string: - -```php -use Hypervel\Support\Str; - -$plural = Str::of('child')->plural(2); - -// children - -$plural = Str::of('child')->plural(1); - -// child -``` - - -#### position - -The `position` method returns the position of the first occurrence of a substring in a string. If the substring does not exist within the string, `false` is returned: - -```php -use Hypervel\Support\Str; - -$position = Str::of('Hello, World!')->position('Hello'); - -// 0 - -$position = Str::of('Hello, World!')->position('W'); - -// 7 -``` - - -#### prepend - -The `prepend` method prepends the given values onto the string: - -```php -use Hypervel\Support\Str; - -$string = Str::of('Framework')->prepend('Hypervel '); - -// Hypervel Framework -``` - - -#### remove - -The `remove` method removes the given value or array of values from the string: - -```php -use Hypervel\Support\Str; - -$string = Str::of('Arkansas is quite beautiful!')->remove('quite'); - -// Arkansas is beautiful! -``` - -You may also pass `false` as a second parameter to ignore case when removing strings. - - -#### repeat - -The `repeat` method repeats the given string: - -```php -use Hypervel\Support\Str; - -$repeated = Str::of('a')->repeat(5); - -// aaaaa -``` - - -#### replace - -The `replace` method replaces a given string within the string: - -```php -use Hypervel\Support\Str; - -$replaced = Str::of('Laravel 6.x')->replace('6.x', '7.x'); - -// Laravel 7.x -``` - -The `replace` method also accepts a `caseSensitive` argument. By default, the `replace` method is case sensitive: - -```php -$replaced = Str::of('macOS 13.x')->replace( - 'macOS', 'iOS', caseSensitive: false -); -``` - - -#### replaceArray - -The `replaceArray` method replaces a given value in the string sequentially using an array: - -```php -use Hypervel\Support\Str; - -$string = 'The event will take place between ? and ?'; - -$replaced = Str::of($string)->replaceArray('?', ['8:30', '9:00']); - -// The event will take place between 8:30 and 9:00 -``` - - -#### replaceFirst - -The `replaceFirst` method replaces the first occurrence of a given value in a string: - -```php -use Hypervel\Support\Str; - -$replaced = Str::of('the quick brown fox jumps over the lazy dog')->replaceFirst('the', 'a'); - -// a quick brown fox jumps over the lazy dog -``` - - -#### replaceLast - -The `replaceLast` method replaces the last occurrence of a given value in a string: - -```php -use Hypervel\Support\Str; - -$replaced = Str::of('the quick brown fox jumps over the lazy dog')->replaceLast('the', 'a'); - -// the quick brown fox jumps over a lazy dog -``` - - -#### replaceMatches - -The `replaceMatches` method replaces all portions of a string matching a pattern with the given replacement string: - -```php -use Hypervel\Support\Str; - -$replaced = Str::of('(+1) 501-555-1000')->replaceMatches('/[^A-Za-z0-9]++/', '') - -// '15015551000' -``` - -The `replaceMatches` method also accepts a closure that will be invoked with each portion of the string matching the given pattern, allowing you to perform the replacement logic within the closure and return the replaced value: - -```php -use Hypervel\Support\Str; - -$replaced = Str::of('123')->replaceMatches('/\d/', function (array $matches) { - return '['.$matches[0].']'; -}); - -// '[1][2][3]' -``` - - -#### replaceStart - -The `replaceStart` method replaces the first occurrence of the given value only if the value appears at the start of the string: - -```php -use Hypervel\Support\Str; - -$replaced = Str::of('Hello World')->replaceStart('Hello', 'Laravel'); - -// Laravel World - -$replaced = Str::of('Hello World')->replaceStart('World', 'Laravel'); - -// Hello World -``` - - -#### replaceEnd - -The `replaceEnd` method replaces the last occurrence of the given value only if the value appears at the end of the string: - -```php -use Hypervel\Support\Str; - -$replaced = Str::of('Hello World')->replaceEnd('World', 'Laravel'); - -// Hello Laravel - -$replaced = Str::of('Hello World')->replaceEnd('Hello', 'Laravel'); - -// Hello World -``` - - -#### scan - -The `scan` method parses input from a string into a collection according to a format supported by the [`sscanf` PHP function](https://www.php.net/manual/en/function.sscanf.php): - -```php -use Hypervel\Support\Str; - -$collection = Str::of('filename.jpg')->scan('%[^.].%s'); - -// collect(['filename', 'jpg']) -``` - - -#### singular - -The `singular` method converts a string to its singular form. This function supports [any of the languages support by Laravel's pluralizer](/docs/localization#pluralization-language): - -```php -use Hypervel\Support\Str; - -$singular = Str::of('cars')->singular(); - -// car - -$singular = Str::of('children')->singular(); - -// child -``` - - -#### slug - -The `slug` method generates a URL friendly "slug" from the given string: - -```php -use Hypervel\Support\Str; - -$slug = Str::of('Hypervel Framework')->slug('-'); - -// hypervel-framework -``` - - -#### snake - -The `snake` method converts the given string to `snake_case`: - -```php -use Hypervel\Support\Str; - -$converted = Str::of('fooBar')->snake(); - -// foo_bar -``` - - -#### split - -The `split` method splits a string into a collection using a regular expression: - -```php -use Hypervel\Support\Str; - -$segments = Str::of('one, two, three')->split('/[\s,]+/'); - -// collect(["one", "two", "three"]) -``` - - -#### squish - -The `squish` method removes all extraneous white space from a string, including extraneous white space between words: - -```php -use Hypervel\Support\Str; - -$string = Str::of(' Hypervel framework ')->squish(); - -// Hypervel framework -``` - - -#### start - -The `start` method adds a single instance of the given value to a string if it does not already start with that value: - -```php -use Hypervel\Support\Str; - -$adjusted = Str::of('this/string')->start('/'); - -// /this/string - -$adjusted = Str::of('/this/string')->start('/'); - -// /this/string -``` - - -#### startsWith - -The `startsWith` method determines if the given string begins with the given value: - -```php -use Hypervel\Support\Str; - -$result = Str::of('This is my name')->startsWith('This'); - -// true -``` - - -#### stripTags - -The `stripTags` method removes all HTML and PHP tags from a string: - -```php -use Hypervel\Support\Str; - -$result = Str::of('Taylor Otwell')->stripTags(); - -// Taylor Otwell - -$result = Str::of('Taylor Otwell')->stripTags(''); - -// Taylor Otwell -``` - - -#### studly - -The `studly` method converts the given string to `StudlyCase`: - -```php -use Hypervel\Support\Str; - -$converted = Str::of('foo_bar')->studly(); - -// FooBar -``` - - -#### substr - -The `substr` method returns the portion of the string specified by the given start and length parameters: - -```php -use Hypervel\Support\Str; - -$string = Str::of('Laravel Framework')->substr(8); - -// Framework - -$string = Str::of('Laravel Framework')->substr(8, 5); - -// Frame -``` - - -#### substrReplace - -The `substrReplace` method replaces text within a portion of a string, starting at the position specified by the second argument and replacing the number of characters specified by the third argument. Passing `0` to the method's third argument will insert the string at the specified position without replacing any of the existing characters in the string: - -```php -use Hypervel\Support\Str; - -$string = Str::of('1300')->substrReplace(':', 2); - -// 13: - -$string = Str::of('The Framework')->substrReplace(' Laravel', 3, 0); - -// The Laravel Framework -``` - - -#### swap - -The `swap` method replaces multiple values in the string using PHP's `strtr` function: - -```php -use Hypervel\Support\Str; - -$string = Str::of('Tacos are great!') - ->swap([ - 'Tacos' => 'Burritos', - 'great' => 'fantastic', - ]); - -// Burritos are fantastic! -``` - - -#### take - -The `take` method returns a specified number of characters from the beginning of the string: - -```php -use Hypervel\Support\Str; - -$taken = Str::of('Build something amazing!')->take(5); - -// Build -``` - - -#### tap - -The `tap` method passes the string to the given closure, allowing you to examine and interact with the string while not affecting the string itself. The original string is returned by the `tap` method regardless of what is returned by the closure: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('Laravel') - ->append(' Framework') - ->tap(function (Stringable $string) { - dump('String after append: '.$string); - }) - ->upper(); - -// LARAVEL FRAMEWORK -``` - - -#### test - -The `test` method determines if a string matches the given regular expression pattern: - -```php -use Hypervel\Support\Str; - -$result = Str::of('Laravel Framework')->test('/Laravel/'); - -// true -``` - - -#### title - -The `title` method converts the given string to `Title Case`: - -```php -use Hypervel\Support\Str; - -$converted = Str::of('a nice title uses the correct case')->title(); - -// A Nice Title Uses The Correct Case -``` - - -#### toBase64 - -The `toBase64` method converts the given string to Base64: - -```php -use Hypervel\Support\Str; - -$base64 = Str::of('Laravel')->toBase64(); - -// TGFyYXZlbA== -``` - - -#### trim - -The `trim` method trims the given string. Unlike PHP's native `trim` function, Laravel's `trim` method also removes unicode whitespace characters: - -```php -use Hypervel\Support\Str; - -$string = Str::of(' Hypervel ')->trim(); - -// 'Hypervel' - -$string = Str::of('/Hypervel/')->trim('/'); - -// 'Hypervel' -``` - - -#### ltrim - -The `ltrim` method trims the left side of the string. Unlike PHP's native `ltrim` function, Laravel's `ltrim` method also removes unicode whitespace characters: - -```php -use Hypervel\Support\Str; - -$string = Str::of(' Hypervel ')->ltrim(); - -// 'Hypervel ' - -$string = Str::of('/Hypervel/')->ltrim('/'); - -// 'Hypervel/' -``` - - -#### rtrim - -The `rtrim` method trims the right side of the given string. Unlike PHP's native `rtrim` function, Laravel's `rtrim` method also removes unicode whitespace characters: - -```php -use Hypervel\Support\Str; - -$string = Str::of(' Hypervel ')->rtrim(); - -// ' Hypervel' - -$string = Str::of('/Hypervel/')->rtrim('/'); - -// '/Hypervel' -``` - - -#### ucfirst - -The `ucfirst` method returns the given string with the first character capitalized: - -```php -use Hypervel\Support\Str; - -$string = Str::of('foo bar')->ucfirst(); - -// Foo bar -``` - - -#### ucsplit - -The `ucsplit` method splits the given string into a collection by uppercase characters: - -```php -use Hypervel\Support\Str; - -$string = Str::of('Foo Bar')->ucsplit(); - -// collect(['Foo', 'Bar']) -``` - - -#### unwrap - -The `unwrap` method removes the specified strings from the beginning and end of a given string: - -```php -use Hypervel\Support\Str; - -Str::of('-Laravel-')->unwrap('-'); - -// Laravel - -Str::of('{framework: "Laravel"}')->unwrap('{', '}'); - -// framework: "Laravel" -``` - - -#### upper - -The `upper` method converts the given string to uppercase: - -```php -use Hypervel\Support\Str; - -$adjusted = Str::of('laravel')->upper(); - -// LARAVEL -``` - - -#### when - -The `when` method invokes the given closure if a given condition is `true`. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('Taylor') - ->when(true, function (Stringable $string) { - return $string->append(' Otwell'); - }); - -// 'Taylor Otwell' -``` - -If necessary, you may pass another closure as the third parameter to the `when` method. This closure will execute if the condition parameter evaluates to `false`. - - -#### whenContains - -The `whenContains` method invokes the given closure if the string contains the given value. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('tony stark') - ->whenContains('tony', function (Stringable $string) { - return $string->title(); - }); - -// 'Tony Stark' -``` - -If necessary, you may pass another closure as the third parameter to the `when` method. This closure will execute if the string does not contain the given value. - -You may also pass an array of values to determine if the given string contains any of the values in the array: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('tony stark') - ->whenContains(['tony', 'hulk'], function (Stringable $string) { - return $string->title(); - }); - -// Tony Stark -``` - - -#### whenContainsAll - -The `whenContainsAll` method invokes the given closure if the string contains all of the given sub-strings. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('tony stark') - ->whenContainsAll(['tony', 'stark'], function (Stringable $string) { - return $string->title(); - }); - -// 'Tony Stark' -``` - -If necessary, you may pass another closure as the third parameter to the `when` method. This closure will execute if the condition parameter evaluates to `false`. - - -#### whenEmpty - -The `whenEmpty` method invokes the given closure if the string is empty. If the closure returns a value, that value will also be returned by the `whenEmpty` method. If the closure does not return a value, the fluent string instance will be returned: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of(' ')->whenEmpty(function (Stringable $string) { - return $string->trim()->prepend('Laravel'); -}); - -// 'Laravel' -``` - - -#### whenNotEmpty - -The `whenNotEmpty` method invokes the given closure if the string is not empty. If the closure returns a value, that value will also be returned by the `whenNotEmpty` method. If the closure does not return a value, the fluent string instance will be returned: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('Framework')->whenNotEmpty(function (Stringable $string) { - return $string->prepend('Hypervel '); -}); - -// 'Hypervel Framework' -``` - - -#### whenStartsWith - -The `whenStartsWith` method invokes the given closure if the string starts with the given sub-string. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('disney world')->whenStartsWith('disney', function (Stringable $string) { - return $string->title(); -}); - -// 'Disney World' -``` - - -#### whenEndsWith - -The `whenEndsWith` method invokes the given closure if the string ends with the given sub-string. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('disney world')->whenEndsWith('world', function (Stringable $string) { - return $string->title(); -}); - -// 'Disney World' -``` - - -#### whenExactly - -The `whenExactly` method invokes the given closure if the string exactly matches the given string. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('laravel')->whenExactly('laravel', function (Stringable $string) { - return $string->title(); -}); - -// 'Laravel' -``` - - -#### whenNotExactly - -The `whenNotExactly` method invokes the given closure if the string does not exactly match the given string. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('framework')->whenNotExactly('laravel', function (Stringable $string) { - return $string->title(); -}); - -// 'Framework' -``` - - -#### whenIs - -The `whenIs` method invokes the given closure if the string matches a given pattern. Asterisks may be used as wildcard values. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('foo/bar')->whenIs('foo/*', function (Stringable $string) { - return $string->append('/baz'); -}); - -// 'foo/bar/baz' -``` - - -#### whenIsAscii - -The `whenIsAscii` method invokes the given closure if the string is 7 bit ASCII. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('laravel')->whenIsAscii(function (Stringable $string) { - return $string->title(); -}); - -// 'Laravel' -``` - - -#### whenIsUlid - -The `whenIsUlid` method invokes the given closure if the string is a valid ULID. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; - -$string = Str::of('01gd6r360bp37zj17nxb55yv40')->whenIsUlid(function (Stringable $string) { - return $string->substr(0, 8); -}); - -// '01gd6r36' -``` - - -#### whenIsUuid - -The `whenIsUuid` method invokes the given closure if the string is a valid UUID. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('a0a2a2d2-0b87-4a18-83f2-2529882be2de')->whenIsUuid(function (Stringable $string) { - return $string->substr(0, 8); -}); - -// 'a0a2a2d2' -``` - - -#### whenTest - -The `whenTest` method invokes the given closure if the string matches the given regular expression. The closure will receive the fluent string instance: - -```php -use Hypervel\Support\Str; -use Hypervel\Support\Stringable; - -$string = Str::of('laravel framework')->whenTest('/laravel/', function (Stringable $string) { - return $string->title(); -}); - -// 'Laravel Framework' -``` - - -#### wordCount - -The `wordCount` method returns the number of words that a string contains: - -```php -use Hypervel\Support\Str; - -Str::of('Hello, world!')->wordCount(); // 2 -``` - - -#### words - -The `words` method limits the number of words in a string. If necessary, you may specify an additional string that will be appended to the truncated string: - -```php -use Hypervel\Support\Str; - -$string = Str::of('Perfectly balanced, as all things should be.')->words(3, ' >>>'); - -// Perfectly balanced, as >>> -``` - - -#### wrap - -The `wrap` method wraps the given string with an additional string or pair of strings: - -```php -use Hypervel\Support\Str; - -Str::of('Laravel')->wrap('"'); - -// "Laravel" - -Str::is('is')->wrap(before: 'This ', after: ' Laravel!'); - -// This is Laravel! -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/structure.md b/_archive/docs/0.3/docs/structure.md deleted file mode 100644 index 5f4612abca..0000000000 --- a/_archive/docs/0.3/docs/structure.md +++ /dev/null @@ -1,174 +0,0 @@ -# Directory Structure -[[toc]] - -## Introduction - -The default Hypervel application structure follows by Laravel. It can provide you a great starting point for both large and small applications. You're free to organize your application however you like. But some directories such as `bootstrap`, `config`, `database` and `runtime` should remain due to the core design in Hypervel. - -Here's how you directory structure looks like: - -```plaintext:no-line-numbers -├── app -│   ├── Console -│   │   └── Commands -│   ├── Events -│   ├── Exceptions -│   ├── Http -│   │   ├── Controllers -│   │   ├── Middleware -│   │   └── Requests -| |── Jobs -│   ├── Listeners -│   ├── Models -│   └── Providers -├── bootstrap -├── config -├── database -│   ├── factories -│   ├── migrations -│   └── seeders -├── lang -│   ├── en -│   ├── zh_CN -│   └── zh_TW -|── public -|── resources -├── routes -├── runtime -│   └── container -├── storage -│   ├── app -|   ├── framework -│   └── logs -└── tests - ├── Feature - └── Unit -``` - -## The Root Directory - -### The App Directory - -The `app` directory contains the core code of your application. We'll explore this directory in more detail soon; however, almost all of the classes in your application will be in this directory. - -### The Bootstrap Directory - -The `bootstrap` directory contains the `app.php` file which bootstraps the framework. - -### The Runtime Directory - -This directory stores files generated by Hypervel during runtime. It houses a `container` directory which contains framework generated files for caching the scanning result. - -### The Config Directory - -The `config` directory, as the name implies, contains all of your application's configuration files. It's a great idea to read through all of these files and familiarize yourself with all of the options available to you. - -::: warning -There's additional `autoload` directory inside `config` in Hyperf. This folder is removed in Hypervel. All the config files are flattened as Laravel does. - -`hyperf.php` is preserved for the compatibility of root configs in Hyperf. All the configs here will not include any prefixes. And all the configs in `config/config.php` are moved to `config/app.php` in Hypervel. -::: - -### The Database Directory - -The `database` directory contains your database migrations, model factories, and seeds. If you wish, you may also use this directory to hold an SQLite database. - -### The Public Directory - -The public directory houses your static assets such as images, JavaScript, and CSS. - -### The Resources Directory - -The `resources` directory contains your [views](/docs/views) as well as your raw, un-compiled assets such as CSS or JavaScript. - -### The Lang Directory - -The `lang` directory contains the translation files. Within this directory, there may be subdirectories for each language supported by the application. This is the approach Hypervel uses to manage translation strings for built-in features such as validation error messages. - -### The Routes Directory - -The `routes` directory contains all of the route definitions for your application. By default, three route files are included with Hypervel: `web.php`, `api.php` and `console.php`. - -The `web.php` file contains routes that serves common requests such as views, files in Hypervel. - -The `api.php` file contains routes that are intended to be stateless, so requests entering the application through these routes are intended to be authenticated and will not have access to session state. - -::: info -Middleware groups are applied to `web.php` and `api.php`, but are kept in empty by default. You can configure them manually if in need. -::: - -The `console.php` file is where you may define all of your closure based console commands. Each closure is bound to a command instance allowing a simple approach to interacting with each command's IO methods. Even though this file does not define HTTP routes, it defines console based entry points (routes) into your application. You may also [schedule](/docs/scheduling) tasks in the `console.php` file. - -### The Storage Directory - -The `storage` directory contains your logs, compiled Blade templates, file caches, and other files generated by the framework. This directory is segregated into `app`, `framework`, and `logs` directories. The `app` directory may be used to store any files generated by your application. The `framework` directory is used to store caches (and framework generated files in the future). Finally, the `logs` directory contains your application's log files. - -The `storage/app/public` directory may be used to store user-generated files, such as profile avatars, that should be publicly accessible. You should create a symbolic link at `public/storage` which points to this directory. - -### The Tests Directory - -The `tests` directory contains your automated tests. Example [PHPUnit](https://phpunit.de/) unit tests and feature tests are provided out of the box. Each test class should be suffixed with the word `Test`. You may run your tests using the `/vendor/bin/phpunit` commands. - -::: note -Pest package is not supported yet. -::: - -## The App Directory - -The majority of your application is housed in the `app` directory. By default, this directory is namespaced under `App` and is autoloaded by Composer using the [PSR-4 autoloading standard](https://www.php-fig.org/psr/psr-4/). - -The `app` directory contains a variety of additional directories such as `Console`, `Http`, and `Providers`. Think of the `Console` and `Http` directories as providing an API into the core of your application. The HTTP protocol and CLI are both mechanisms to interact with your application, but do not actually contain application logic. In other words, they are two ways of issuing commands to your application. The `Console` directory contains all of your Artisan commands, while the `Http` directory contains your controllers, middleware, and requests. - -::: note -Many of the classes in the `app` directory can be generated by Hypervel via commands. To review the available commands, run the `php artisan list make` command in your terminal. -::: - -### The Console Directory - -The `Console` directory contains all of the custom Artisan commands for your application. These commands may be generated using the `make:command` command. This directory also houses your console kernel, which is where your custom Artisan commands are registered and your [scheduled tasks](/docs/scheduling) are defined. - -### The Events Directory - -The `Events` directory houses [event classes](/docs/events). Events may be used to alert other parts of your application that a given action has occurred, providing a great deal of flexibility and decoupling. - -### The Exceptions Directory - -The `Exceptions` directory contains your application's exception handler and is also a good place to place any exceptions thrown by your application. If you would like to customize how your exceptions are logged or rendered, you should modify the Handler class in this directory. - -### The Http Directory - -The `Http` directory contains your controllers, middleware, and form requests. Almost all of the logic to handle requests entering your application will be placed in this directory. - -### The Jobs Directory - -This directory does not exist by default, but will be created for you if you execute the `make:job` Artisan command. The `Jobs` directory houses the [queueable jobs](/docs/queues) for your application. Jobs may be queued by your application or run synchronously within the current request lifecycle. Jobs that run synchronously during the current request are sometimes referred to as "commands" since they are an implementation of the [command pattern](https://en.wikipedia.org/wiki/Command_pattern). - -### The Listeners Directory - -The `Listeners` directory contains the classes that handle your [events](/docs/events). Event listeners receive an event instance and perform logic in response to the event being fired. For example, a `UserRegistered` event might be handled by a `SendWelcomeEmail` listener. If you execute the `make:listener` command, listener classes will be placed in this directory. - -### The Mail Directory - -This directory does not exist by default, but will be created for you if you execute the `make:mail` Artisan command. The `Mail` directory contains all of your [classes that represent emails](/docs/mail) sent by your application. Mail objects allow you to encapsulate all of the logic of building an email in a single, simple class that may be sent using the `Mail::send` method. - -### The Models Directory - -The `Models` directory contains all of your [Eloquent model classes](/docs/eloquent). The Eloquent ORM included with Hypervel provides a beautiful, simple ActiveRecord implementation for working with your database. Each database table has a corresponding "Model" which is used to interact with that table. Models allow you to query for data in your tables, as well as insert new records into the table. - -### The Notifications Directory - -This directory does not exist by default, but will be created for you if you execute the `make:notification` Artisan command. The `Notifications` directory contains all of the "transactional" [notifications](/docs/notifications) that are sent by your application, such as simple notifications about events that happen within your application. Laravel's notification feature abstracts sending notifications over a variety of drivers such as email, Slack, SMS, or stored in a database. - -### The Policies Directory - -This directory does not exist by default, but will be created for you if you execute the `make:policy` Artisan command. The `Policies` directory contains the [authorization policy classes](/docs/authorization) for your application. Policies are used to determine if a user can perform a given action against a resource. - -### The Providers Directory - -The `Providers` directory contains all of the [service providers](/docs/providers) for your application. Service providers bootstrap your application by binding services in the service container, registering events, or performing any other tasks to prepare your application for incoming requests. - -In a fresh Hypervel application, this directory will already contain several providers. You are free to add your own providers to this directory as needed. - -### The Rules Directory - -This directory does not exist by default, but will be created for you if you execute the `make:rule` Artisan command. The `Rules` directory contains the custom validation rule objects for your application. Rules are used to encapsulate complicated validation logic in a simple object. For more information, check out the [validation documentation](/docs/validation). \ No newline at end of file diff --git a/_archive/docs/0.3/docs/telescope.md b/_archive/docs/0.3/docs/telescope.md deleted file mode 100644 index 211900caf2..0000000000 --- a/_archive/docs/0.3/docs/telescope.md +++ /dev/null @@ -1,404 +0,0 @@ -# Hypervel Telescope -[[toc]] - -## Introduction - -[Hypervel Telescope](https://github.com/hypervel/telescope) makes a wonderful companion to your local Hypervel development environment. Telescope provides insight into the requests coming into your application, exceptions, log entries, database queries, queued jobs, mail, notifications, cache operations, scheduled tasks, variable dumps, and more. - - - -## Installation - -You may use the Composer package manager to install Telescope into your Hypervel project: - -```shell -composer require hypervel/telescope -``` - -After installing Telescope, publish its assets and migrations using the `vendor:publish` Artisan command. After installing Telescope, you should also run the `migrate` command in order to create the tables needed to store Telescope's data: - -```shell -php artisan vendor:publish "Hypervel\Telescope\TelescopeServiceProvider" - -php artisan migrate -``` - -Finally, you may access the Telescope dashboard via the `/telescope` route. - -### Local Only Installation - -If you plan to only use Telescope to assist your local development, you may install Telescope using the `--dev` flag: - -```shell -composer require hypervel/telescope --dev - -php artisan vendor:publish "Hypervel\Telescope\TelescopeServiceProvider" - -php artisan migrate -``` - -After running `vendor:publish`, you should remove the `TelescopeServiceProvider` service provider registration from your application's `bootstrap/providers.php` configuration file. Instead, manually register Telescope's service providers in the `register` method of your `App\Providers\AppServiceProvider` class. We will ensure the current environment is `local` before registering the providers: - -```php -/** - * Register any application services. - */ -public function register(): void -{ - if ($this->app->environment('local') && class_exists(\Hypervel\Telescope\TelescopeServiceProvider::class)) { - $this->app->register(\Hypervel\Telescope\TelescopeServiceProvider::class); - $this->app->register(TelescopeServiceProvider::class); - } -} -``` - -Finally, you should also prevent the Telescope package from being [auto-discovered](/docs/packages#package-discovery) by adding the following to your `composer.json` file: - -```json -"extra": { - "hypervel": { - "dont-discover": [ - "hypervel/telescope" - ] - } -}, -``` - -### Configuration - -After publishing Telescope's assets, its primary configuration file will be located at `config/telescope.php`. This configuration file allows you to configure your [watcher options](#available-watchers). Each configuration option includes a description of its purpose, so be sure to thoroughly explore this file. - -If desired, you may disable Telescope's data collection entirely using the `enabled` configuration option: - -```php -'enabled' => env('TELESCOPE_ENABLED', true), -``` - -### Data Pruning - -Without pruning, the `telescope_entries` table can accumulate records very quickly. To mitigate this, you should [schedule](/docs/scheduling) the `telescope:prune` Artisan command to run daily: - -```php -use Hypervel\Support\Facades\Schedule; - -Schedule::command('telescope:prune')->daily(); -``` - -By default, all entries older than 24 hours will be pruned. You may use the `hours` option when calling the command to determine how long to retain Telescope data. For example, the following command will delete all records created over 48 hours ago: - -```php -use Hypervel\Support\Facades\Schedule; - -Schedule::command('telescope:prune --hours=48')->daily(); -``` - -### Dashboard Authorization - -The Telescope dashboard may be accessed via the `/telescope` route. By default, you will only be able to access this dashboard in the `local` environment. Within your `app/Providers/TelescopeServiceProvider.php` file, there is an [authorization gate](/docs/authorization#gates) definition. This authorization gate controls access to Telescope in **non-local** environments. You are free to modify this gate as needed to restrict access to your Telescope installation: - -```php -use App\Models\User; - -/** - * Register the Telescope gate. - * - * This gate determines who can access Telescope in non-local environments. - */ -protected function gate(): void -{ - Gate::define('viewTelescope', function (User $user) { - return in_array($user->email, [ - 'albert@hypervel.org', - ]); - }); -} -``` - -::: warning -You should ensure you change your `APP_ENV` environment variable to `production` in your production environment. Otherwise, your Telescope installation will be publicly available. -::: - -## Filtering - -### Entries - -You may filter the data that is recorded by Telescope via the `filter` closure that is defined in your `App\Providers\TelescopeServiceProvider` class. By default, this closure records all data in the `local` environment and exceptions, failed jobs, scheduled tasks, and data with monitored tags in all other environments: - -```php -use Hypervel\Telescope\IncomingEntry; -use Hypervel\Telescope\Telescope; - -/** - * Register any application services. - */ -public function register(): void -{ - $this->hideSensitiveRequestDetails(); - - Telescope::filter(function (IncomingEntry $entry) { - if ($this->app->environment('local')) { - return true; - } - - return $entry->isReportableException() || - $entry->isFailedJob() || - $entry->isScheduledTask() || - $entry->isSlowQuery() || - $entry->hasMonitoredTag(); - }); -} -``` - -### Batches - -While the `filter` closure filters data for individual entries, you may use the `filterBatch` method to register a closure that filters all data for a given request or console command. If the closure returns `true`, all of the entries are recorded by Telescope: - -```php -use Hypervel\Support\Collection; -use Hypervel\Telescope\IncomingEntry; -use Hypervel\Telescope\Telescope; - -/** - * Register any application services. - */ -public function register(): void -{ - $this->hideSensitiveRequestDetails(); - - Telescope::filterBatch(function (Collection $entries) { - if ($this->app->environment('local')) { - return true; - } - - return $entries->contains(function (IncomingEntry $entry) { - return $entry->isReportableException() || - $entry->isFailedJob() || - $entry->isScheduledTask() || - $entry->isSlowQuery() || - $entry->hasMonitoredTag(); - }); - }); -} -``` - -## Tagging - -Telescope allows you to search entries by "tag". Often, tags are Eloquent model class names or authenticated user IDs which Telescope automatically adds to entries. Occasionally, you may want to attach your own custom tags to entries. To accomplish this, you may use the `Telescope::tag` method. The `tag` method accepts a closure which should return an array of tags. The tags returned by the closure will be merged with any tags Telescope would automatically attach to the entry. Typically, you should call the `tag` method within the `register` method of your `App\Providers\TelescopeServiceProvider` class: - -```php -use Hypervel\Telescope\IncomingEntry; -use Hypervel\Telescope\Telescope; - -/** - * Register any application services. - */ -public function register(): void -{ - $this->hideSensitiveRequestDetails(); - - Telescope::tag(function (IncomingEntry $entry) { - return $entry->type === 'request' - ? ['status:'.$entry->content['response_status']] - : []; - }); -} -``` - -## Available Watchers - -Telescope "watchers" gather application data when a request or console command is executed. You may customize the list of watchers that you would like to enable within your `config/telescope.php` configuration file: - -```php -'watchers' => [ - Watchers\CacheWatcher::class => true, - Watchers\CommandWatcher::class => true, - // ... -], -``` - -Some watchers also allow you to provide additional customization options: - -```php -'watchers' => [ - Watchers\QueryWatcher::class => [ - 'enabled' => env('TELESCOPE_QUERY_WATCHER', true), - 'slow' => 100, - ], - // ... -], -``` - -### Batch Watcher - -The batch watcher records information about queued [batches](/docs/queues#job-batching), including the job and connection information. - -### Cache Watcher - -The cache watcher records data when a cache key is hit, missed, updated and forgotten. - -### Client Request Watcher - -The client request watcher records all HTTP requests sent via Guzzle's HTTP client. - -### Command Watcher - -The command watcher records the arguments, options, exit code, and output whenever an Artisan command is executed. If you would like to exclude certain commands from being recorded by the watcher, you may specify the command in the `ignore` option within your `config/telescope.php` file: - -```php -'watchers' => [ - Watchers\CommandWatcher::class => [ - 'enabled' => env('TELESCOPE_COMMAND_WATCHER', true), - 'ignore' => ['key:generate'], - ], - // ... -] -``` - -### Dump Watcher - -The dump watcher records and displays your variable dumps in Telescope. When using Hypervel, variables may be dumped using the global `dump` function. The dump watcher tab must be open in a browser for the dump to be recorded, otherwise, the dumps will be ignored by the watcher. - -### Event Watcher - -The event watcher records the payload, listeners, and broadcast data for any [events](/docs/events) dispatched by your application. The Hypervel framework's internal events are ignored by the Event watcher. - -### Exception Watcher - -The exception watcher records the data and stack trace for any reportable exceptions that are thrown by your application. - -### Gate Watcher - -The gate watcher records the data and result of [gate and policy](/docs/authorization) checks by your application. If you would like to exclude certain abilities from being recorded by the watcher, you may specify those in the `ignore_abilities` option in your `config/telescope.php` file: - -```php -'watchers' => [ - Watchers\GateWatcher::class => [ - 'enabled' => env('TELESCOPE_GATE_WATCHER', true), - 'ignore_abilities' => ['viewNova'], - ], - // ... -], -``` - -### HTTP Client Watcher - -The HTTP client watcher records outgoing [HTTP client requests](/docs/http-client) made by your application. - -### Job Watcher - -The job watcher records the data and status of any [jobs](/docs/queues) dispatched by your application. - -### Log Watcher - -The log watcher records the [log data](/docs/logging) for any logs written by your application. - -By default, Telescope will only record logs at the `error` level and above. However, you can modify the `level` option in your application's `config/telescope.php` configuration file to modify this behavior: - -```php -'watchers' => [ - Watchers\LogWatcher::class => [ - 'enabled' => env('TELESCOPE_LOG_WATCHER', true), - 'level' => 'debug', - ], - - // ... -], -``` - -### Mail Watcher - -The mail watcher allows you to view an in-browser preview of [emails](/docs/mail) sent by your application along with their associated data. You may also download the email as an `.eml` file. - -### Model Watcher - -The model watcher records model changes whenever an Eloquent [model event](/docs/eloquent#events) is dispatched. You may specify which model events should be recorded via the watcher's `events` option: - -```php -'watchers' => [ - Watchers\ModelWatcher::class => [ - 'enabled' => env('TELESCOPE_MODEL_WATCHER', true), - 'events' => ['eloquent.created*', 'eloquent.updated*'], - ], - // ... -], -``` - -If you would like to record the number of models hydrated during a given request, enable the `hydrations` option: - -```php -'watchers' => [ - Watchers\ModelWatcher::class => [ - 'enabled' => env('TELESCOPE_MODEL_WATCHER', true), - 'events' => ['eloquent.created*', 'eloquent.updated*'], - 'hydrations' => true, - ], - // ... -], -``` - -### Notification Watcher - -The notification watcher records all [notifications](/docs/notifications) sent by your application. If the notification triggers an email and you have the mail watcher enabled, the email will also be available for preview on the mail watcher screen. - -### Query Watcher - -The query watcher records the raw SQL, bindings, and execution time for all queries that are executed by your application. The watcher also tags any queries slower than 100 milliseconds as `slow`. You may customize the slow query threshold using the watcher's `slow` option: - -```php -'watchers' => [ - Watchers\QueryWatcher::class => [ - 'enabled' => env('TELESCOPE_QUERY_WATCHER', true), - 'slow' => 50, - ], - // ... -], -``` - -### Redis Watcher - -The Redis watcher records all [Redis](/docs/redis) commands executed by your application. If you are using Redis for caching, cache commands will also be recorded by the Redis watcher. - -### Request Watcher - -The request watcher records the request, headers, session, and response data associated with any requests handled by the application. You may limit your recorded response data via the `size_limit` (in kilobytes) option: - -```php -'watchers' => [ - Watchers\RequestWatcher::class => [ - 'enabled' => env('TELESCOPE_REQUEST_WATCHER', true), - 'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64), - ], - // ... -], -``` - -### Schedule Watcher - -The schedule watcher records the command and output of any [scheduled tasks](/docs/scheduling) run by your application. - -### View Watcher - -The view watcher records the [view](/docs/views) name, path, data, and "composers" used when rendering views. - -## Displaying User Avatars - -The Telescope dashboard displays the user avatar for the user that was authenticated when a given entry was saved. By default, Telescope will retrieve avatars using the Gravatar web service. However, you may customize the avatar URL by registering a callback in your `App\Providers\TelescopeServiceProvider` class. The callback will receive the user's ID and email address and should return the user's avatar image URL: - -```php -use App\Models\User; -use Hypervel\Telescope\Telescope; - -/** - * Register any application services. - */ -public function register(): void -{ - // ... - - Telescope::avatar(function (?string $id, ?string $email) { - return ! is_null($id) - ? '/avatars/'.User::find($id)->avatar_path - : '/generic-avatar.jpg'; - }); -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/testbench.md b/_archive/docs/0.3/docs/testbench.md deleted file mode 100644 index 46df99d72e..0000000000 --- a/_archive/docs/0.3/docs/testbench.md +++ /dev/null @@ -1,141 +0,0 @@ -# Packages Toolkit: Testbench -[[toc]] - -## Introduction - -Packages Toolkit for Hypervel is a collection of packages that have been designed to write, test, and preview your Hypervel packages. - -Before going through the rest of this documentation, please take some time to read the [Package Development](/docs/packages) section of Hypervel's own documentation, if you haven't done so yet. - -## Installation - -To start using Packages Toolkit for Hypervel you can first install the components: - -```shell:no-line-numbers -composer require --dev hypervel/testbench -``` - -## Configuration - -The Packages Toolkit for Hypervel uses `testbench.yaml` as a configuration file where you can define the following schemas to be used within Workbench environment: - - Name | Type | Description -:----------------|:----------|:-------------------- - `hypervel` | `string` | Set the path to Hypervel skeleton. - `providers` | `array` | List of Service Provider classes to be loaded. - `dont-discover` | `array` | List of packages to be ignored. - `env` | `array` | Set environment variables to be loaded. - `purge` | `array` | Configurable `files` and `directories` to be pruned after executing test cases. - `workbench` | Object | See [Workbench configuration](#workbench-discovery-configuration) for details. - -## Basic Usages - -For the `testbench` command to understand any required service providers, bootstrappers, environment variables or other options to be used when executing the "artisan" command you need to add the following `testbench.yaml` file on the project root directory. - -```yaml -providers: - - Hypervel\Package\PackageServiceProvider - -dont-discover: - - hypervel/sanctum -``` - -### Hypervel Skeleton - -You can use `hypervel` configuration key to set a custom location instead of using the default `vendor/hypervel/testbench/workbench`: - -```yaml -hypervel: ./workbench -``` - -To publish the skeleton folder, you can run the following command: - -```shell:no-line-numbers -vendor/bin/testbench-sync workbench -``` - -### Service Providers - -You can use `providers` configuration key to set an array of service providers to be loaded additionally: - -```yaml -providers: - - Hypervel\Sanctum\SanctumServiceProvider - - Workbench\App\Providers\WorkbenchServiceProvider -``` - -### Exclude Packages - -To exclude specific packages, you can also use `dont-discover` to exclude specific packages from being loaded: - -```yaml -dont-discover: - - hypervel/html -``` - -### Environment Variables - -You can use `env` configuration key to set an array of environment variables to be loaded: - -```yaml -env: - - SEND_QUERIES_TO_RAY=(false) -``` - -::: warning -The `env` environment variables are only applied when using the CLI and will not be used when running tests. -::: - -#### Workbench Discovery Configuration - -The `discovers` configuration allows Workbench to discover routes and commands. - - Name | Type | Description -:----------------|:--------------|:-------------------- - `web` | `bool` | Enabling the options allows Workbench to register routes from `workbench/routes/web.php` - `api` | `bool` | Enabling the options allows Workbench to register routes from `workbench/routes/api.php` - `commands` | `bool` | Enabling the options allows Workbench to register console commands from `workbench/routes/console.php` - -#### Example - -```yaml -workbench: - discovers: - web: true - api: false - commands: false -``` - -## Getting Started - -To use Testbench Component, all you need to do is extend `Hypervel\Testbench\TestCase` instead of `PHPUnit\Framework\TestCase`. The fixture app booted by `Hypervel\Testbench\TestCase` is predefined to follow the base application skeleton of Hypervel. - -```php -class TestCase extends \Hypervel\Testbench\TestCase -{ - // -} -``` - -### PHPUnit Configuration - -You can separate your tests in your `phpunit.xml` file by providing different test suites, allowing you to run your Feature tests on demand. - -For example: - -```yaml - - - ./tests/Feature - - - ./tests/Unit - - -``` - -Run only your feature tests by running PHPUnit with the `--testsuite=Feature` option. - -```shell:no-line-numbers -vendor/bin/phpunit --testsuite=Feature -``` diff --git a/_archive/docs/0.3/docs/testing.md b/_archive/docs/0.3/docs/testing.md deleted file mode 100644 index c560493713..0000000000 --- a/_archive/docs/0.3/docs/testing.md +++ /dev/null @@ -1,208 +0,0 @@ -# Testing: Getting Started -[[toc]] - -## Introduction - -Hypervel is built with testing in mind. In fact, support for testing with PHPUnit is included out of the box and a `phpunit.xml` file is already set up for your application. The framework also ships with convenient helper methods that allow you to expressively test your applications. - -By default, your application's `tests` directory contains two directories: `Feature` and `Unit`. Unit tests are tests that focus on a very small, isolated portion of your code. In fact, most unit tests probably focus on a single method. Feature tests may test a larger portion of your code, including how several objects interact with each other or even a full HTTP request to a JSON endpoint. - -An `ExampleTest.php` file is provided in both the `Feature` and `Unit` test directories. After installing a new Hypervel application, run `vendor/bin/phpunit` on the command line to run your tests. - -## Environment - -When running tests via `vendor/bin/phpunit`, Hypervel will automatically set the configuration environment to `testing` because of the environment variables defined in the `phpunit.xml` file. Hypervel also automatically configures the session and cache to the `array` driver while testing, meaning no session or cache data will be persisted while testing. - -You are free to define other testing environment configuration values as necessary. The `testing` environment variables may be configured in the `phpunit.xml` file, but make sure to clear your configuration cache using the `config:clear` Artisan command before running your tests! - -## Creating & Running Tests - -To create a new test case, use the `make:test` Artisan command: - -```shell:no-line-numbers -// Create a test in the Feature directory... -php artisan make:test UserTest - -// Create a test in the Unit directory... -php artisan make:test UserTest --unit -``` - -Once the test has been generated, you may define test methods as you normally would using PHPUnit. To run your tests, execute the `phpunit` or `artisan test` command from your terminal: - -```php -assertTrue(true); - } -} -``` - -::: note -If you define your own `setUp` / `tearDown` methods within a test class, be sure to call the respective `parent::setUp()` / `parent::tearDown()` methods on the parent class. -::: - -## Running Tests in Coroutines - -Sometimes you may want to test your functions running in coroutines. If you extend your testing class from `PHPUnit\Framework\TestCase`, you will have unexpected errors since your logics are not running in coroutines. - -```php -use PHPUnit\Framework\TestCase; - -class UnitTest extends TestCase -{ - public function testCoroutines(): void - { - dump(Coroutine::id()); - } -} -``` - -In the above example you will get `-1` since there's no coroutine container running outside of your application. - -To solve this issue, you can wrap your logics with a coroutine container via `Hypervel\Coroutine\run` function. - -```php -use PHPUnit\Framework\TestCase; - -use function Hypervel\Coroutine\run; - -class UnitTest extends TestCase -{ - public function testCoroutines(): void - { - run(function () { - dump(Coroutine::id()); - }); - } -} -``` - -A more convenient way to make sure all the tests in this test class will be executed in coroutine environment is by using `RunTestsInCoroutine` trait. - -```php -use PHPUnit\Framework\TestCase; - -use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine; - -class UnitTest extends TestCase -{ - use RunTestsInCoroutine; - - public function testCoroutines(): void - { - dump(Coroutine::id()); - } -} -``` - -By this way, coroutine container will be wrapped before executing your test functions except `setUp` and `tearDown`. - -::: info - -If your test class extends `Tests\TestCase`, then `RunTestsInCoroutine` is included by default. You don't need to require it once again. -::: - -### Context in Coroutine Tests - -When you want to test your coroutine components, you can mock your context using `Context` directly. - -```php -use PHPUnit\Framework\TestCase; -use Hypervel\Context\Context; -use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine; - -class UnitTest extends TestCase -{ - use RunTestsInCoroutine; - - public function testCoroutines(): void - { - Context::set('auth_context.users.foo'); - - // your logics which uses related auth context inside - // ... - } -} -``` - -In traditional test cases, we usually prepare our necessary objects or bootstrapping flows in `setUp` function. - -```php -use PHPUnit\Framework\TestCase; -use Hypervel\Context\Context; -use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine; - -class UnitTest extends TestCase -{ - use RunTestsInCoroutine; - - public function setUp(): void - { - Context::set('auth_context.users.foo'); - } - - public function testCoroutines(): void - { - dump(Context::get('auth_context.users.foo')); - } - - public function tearDown(): void - { - Context::destroy('auth_context.users.foo'); - } -} -``` - -Remember what what we mentioned previously that `setUp` and `tearDown` functions will not be executed in coroutines? - -But in the above example you will find you can still get correct mocked context in your test functions. Why is that happening? - -That's because behind `RunTestsInCoroutine`, it automatically copies the context from non-coroutine environment to your current coroutine in the test function. If you'd like to disable this behavior, you can disable it by setting `copyNonCoroutineContext` property in your test class. - -```php -protected bool $copyNonCoroutineContext = false; -``` - -::: warning -However, you still need to be very careful for your context states in non-coroutine environment. Because all the non-coroutine environment share the same context, you need to clean these context states manually. Otherwise it will cause states bleeding. -::: - -Or you can use `invokeSetupInCoroutine` and `invokeTearDownInCoroutine` functions to make sure they're sharing the coroutine context with your test functions. - -```php -use PHPUnit\Framework\TestCase; -use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine; - -class UnitTest extends TestCase -{ - use RunTestsInCoroutine; - - protected function invokeSetupInCoroutine(): void - { - Context::set('auth_context.users.foo'); - } - - protected function invokeTearDownInCoroutine(): void - { - // ... - } - - public function testCoroutines(): void - { - dump(Context::get('auth_context.users.foo')); - } -} -``` \ No newline at end of file diff --git a/_archive/docs/0.3/docs/urls.md b/_archive/docs/0.3/docs/urls.md deleted file mode 100644 index c9073ce095..0000000000 --- a/_archive/docs/0.3/docs/urls.md +++ /dev/null @@ -1,220 +0,0 @@ -# URL Generation -[[toc]] - -## Introduction - -Hypervel provides several helpers to assist you in generating URLs for your application. These helpers are primarily helpful when building links in your templates and API responses, or when generating redirect responses to another part of your application. - -## The Basics - -### Generating URLs - -The `url` helper may be used to generate arbitrary URLs for your application. The generated URL will automatically use the scheme (HTTP or HTTPS) and host from the current request being handled by the application: - -```php -$post = App\Models\Post::find(1); - -echo url("/posts/{$post->id}"); - -// http://example.com/posts/1 -``` - -### Accessing the Current URL - -If no path is provided to the `url` helper, an `Hypervel\Router\UrlGenerator` instance is returned, allowing you to access information about the current URL: - -```php -// Get the current URL without the query string... -echo url()->current(); - -// Get the current URL including the query string... -echo url()->full(); - -// Get the full URL for the previous request... -echo url()->previous(); -``` - -Each of these methods may also be accessed via the `URL` [facade](/docs/facades): - -```php -use Hypervel\Support\Facades\URL; - -echo URL::current(); -``` - -## URLs for Named Routes - -The `route` helper may be used to generate URLs to [named routes](/docs/routing#named-routes). Named routes allow you to generate URLs without being coupled to the actual URL defined on the route. Therefore, if the route's URL changes, no changes need to be made to your calls to the `route` function. For example, imagine your application contains a route defined like the following: - -```php -Route::get('/post/{post}', function (Post $post) { - // ... -}, ['as' => 'post.show']); -``` - -To generate a URL to this route, you may use the `route` helper like so: - -```php -echo route('post.show', ['post' => 1]); - -// http://example.com/post/1 -``` - -Of course, the `route` helper may also be used to generate URLs for routes with multiple parameters: - -```php -Route::get('/post/{post}/comment/{comment}', function (Post $post, Comment $comment) { - // ... -}, ['as' => 'comment.show']); - -echo route('comment.show', ['post' => 1, 'comment' => 3]); - -// http://example.com/post/1/comment/3 -``` - -Any additional array elements that do not correspond to the route's definition parameters will be added to the URL's query string: - -```php -echo route('post.show', ['post' => 1, 'search' => 'rocket']); - -// http://example.com/post/1?search=rocket -``` - -### Signed URLs - -Hypervel allows you to easily create "signed" URLs to named routes. These URLs have a "signature" hash appended to the query string which allows Laravel to verify that the URL has not been modified since it was created. Signed URLs are especially useful for routes that are publicly accessible yet need a layer of protection against URL manipulation. - -For example, you might use signed URLs to implement a public "unsubscribe" link that is emailed to your customers. To create a signed URL to a named route, use the `signedRoute` method of the `URL` facade: - -```php -use Hypervel\Support\Facades\URL; - -return URL::signedRoute('unsubscribe', ['user' => 1]); -``` - -You may exclude the domain from the signed URL hash by providing the absolute argument to the `signedRoute` method: - -```php -return URL::signedRoute('unsubscribe', ['user' => 1], absolute: false); -``` - -If you would like to generate a temporary signed route URL that expires after a specified amount of time, you may use the `temporarySignedRoute` method. When Laravel validates a temporary signed route URL, it will ensure that the expiration timestamp that is encoded into the signed URL has not elapsed: - -```php -use Hypervel\Support\Facades\URL; - -return URL::temporarySignedRoute( - 'unsubscribe', now()->addMinutes(30), ['user' => 1] -); -``` - -#### Validating Signed Route Requests - -To verify that an incoming request has a valid signature, you should call the `hasValidSignature` method on the incoming `Hypervel\Http\Request` instance: - -```php -use Hypervel\Http\Request; - -Route::get('/unsubscribe/{user}', function (Request $request) { - if (! $request->hasValidSignature()) { - abort(401); - } - - // ... -}); -``` - -Sometimes, you may need to allow your application's frontend to append data to a signed URL, such as when performing client-side pagination. Therefore, you can specify request query parameters that should be ignored when validating a signed URL using the `hasValidSignatureWhileIgnoring` method. Remember, ignoring parameters allows anyone to modify those parameters on the request: - -```php -if (! $request->hasValidSignatureWhileIgnoring(['page', 'order'])) { - abort(401); -} -``` - -Instead of validating signed URLs using the incoming request instance, you may assign the `Hypervel\Router\Middleware\ValidateSignature` [middleware](/docs/middleware) to the route. If it is not already present, you may assign this middleware an alias in your HTTP kernel's `$middlewareAliases` array: - -```php -/** - * The application's middleware aliases. - * - * Aliases may be used to conveniently assign middleware to routes and groups. - * - * @var array - */ -protected $middlewareAliases = [ - 'signed' => \Hypervel\Router\Middleware\ValidateSignature::class, -]; -``` - -Once you have registered the middleware in your kernel, you may attach it to a route. If the incoming request does not have a valid signature, the middleware will automatically return a `403` HTTP response: - -```php -Route::post('/unsubscribe/{user}', function (Request $request) { - // ... -}, ['middleware' => 'signed']); -``` - -If your signed URLs do not include the domain in the URL hash, you should provide the `relative` argument to the middleware: - -```php -Route::post('/unsubscribe/{user}', function (Request $request) { - // ... -}, ['middleware' => 'signed:relative']); -``` - -#### Responding to Invalid Signed Routes - -When someone visits a signed URL that has expired, they will receive a generic error page for the `403` HTTP status code. However, you can customize this behavior by defining a custom "renderable" closure for the `InvalidSignatureException` exception in your exception handler. This closure should return an HTTP response: - -```php -use Hypervel\Router\Exceptions\InvalidSignatureException; - -/** - * Register the exception handling callbacks for the application. - */ -public function register(): void -{ - $this->renderable(function (InvalidSignatureException $e) { - return response()->view('error.link-expired', [], 403); - }); -} -``` - -## Fluent URI Objects - -Hypervel's `Uri` class provides a convenient and fluent interface for creating and manipulating URIs via objects. This class wraps the functionality provided by the underlying League URI package and integrates seamlessly with Hypervel's routing system. - -You can create a `Uri` instance easily using static methods: - -```php -use App\Http\Controllers\UserController; -use App\Http\Controllers\InvokableController; -use Illuminate\Support\Uri; - -// Generate a URI instance from the given string... -$uri = Uri::of('https://example.com/path'); - -// Generate URI instances to paths, named routes, or controller actions... -$uri = Uri::to('/dashboard'); -$uri = Uri::route('users.show', ['user' => 1]); -$uri = Uri::signedRoute('users.show', ['user' => 1]); -$uri = Uri::temporarySignedRoute('user.index', now()->addMinutes(5)); - -// Generate a URI instance from the current request URL... -$uri = $request->uri(); -``` - -Once you have a URI instance, you can fluently modify it: - -```php -$uri = Uri::of('https://example.com') - ->withScheme('http') - ->withHost('test.com') - ->withPort(8000) - ->withPath('/users') - ->withQuery(['page' => 2]) - ->withFragment('section-1'); -``` - -For more information on working with fluent URI objects, consult the [URI documentation](/docs/helpers#uri). \ No newline at end of file diff --git a/_archive/docs/0.3/docs/validation.md b/_archive/docs/0.3/docs/validation.md deleted file mode 100644 index 2a212243b0..0000000000 --- a/_archive/docs/0.3/docs/validation.md +++ /dev/null @@ -1,2936 +0,0 @@ -# Validation -[[toc]] - -## Introduction - -Hypervel provides several different approaches to validate your application's incoming data. It is most common to use the `validate` method available on all incoming HTTP requests. However, we will discuss other approaches to validation as well. - -Hypervel includes a wide variety of convenient validation rules that you may apply to data, even providing the ability to validate if values are unique in a given database table. We'll cover each of these validation rules in detail so that you are familiar with all of Hypervel's validation features. - -## Validation Quickstart - -To learn about Hypervel's powerful validation features, let's look at a complete example of validating a form and displaying the error messages back to the user. By reading this high-level overview, you'll be able to gain a good general understanding of how to validate incoming request data using Hypervel: - -### Defining the Routes - -First, let's assume we have the following routes defined in our `routes/web.php` file: - -```php -use App\Http\Controllers\PostController; - -Route::get('/post/create', [PostController::class, 'create']); -Route::post('/post', [PostController::class, 'store']); -``` - -The `GET` route will display a form for the user to create a new blog post, while the `POST` route will store the new blog post in the database. - -### Creating the Controller - -Next, let's take a look at a simple controller that handles incoming requests to these routes. We'll leave the `store` method empty for now: - -```php - $post->id]); - } -} -``` - -### Writing the Validation Logic - -Now we are ready to fill in our `store` method with the logic to validate the new blog post. To do this, we will use the `validate` method provided by the `Hyervel\Http\Request` object. If the validation rules pass, your code will keep executing normally; however, if validation fails, an `Hypervel\Validation\ValidationException` exception will be thrown and the proper error response will automatically be sent back to the user. - -To get a better understanding of the `validate` method, let's jump back into the `store` method: - -```php -/** - * Store a new blog post. - */ -public function store(Request $request): ResponseInterface -{ - $validated = $request->validate([ - 'title' => 'required|unique:posts|max:255', - 'body' => 'required', - ]); - - // The blog post is valid... - - return redirect('/posts'); -} -``` - -As you can see, the validation rules are passed into the `validate` method. Don't worry - all available validation rules are [documented](#available-validation-rules). Again, if the validation fails, the proper response will automatically be generated. If the validation passes, our controller will continue executing normally. - -Alternatively, validation rules may be specified as arrays of rules instead of a single `|` delimited string: - -```php -$validatedData = $request->validate([ - 'title' => ['required', 'unique:posts', 'max:255'], - 'body' => ['required'], -]); -``` - -In addition, you may use the `validateWithBag` method to validate a request and store any error messages within a [named error bag](#named-error-bags): - -```php -$validatedData = $request->validateWithBag('post', [ - 'title' => ['required', 'unique:posts', 'max:255'], - 'body' => ['required'], -]); -``` - -#### Stopping on First Validation Failure - -Sometimes you may wish to stop running validation rules on an attribute after the first validation failure. To do so, assign the `bail` rule to the attribute: - -```php -$request->validate([ - 'title' => 'bail|required|unique:posts|max:255', - 'body' => 'required', -]); -``` - -In this example, if the `unique` rule on the `title` attribute fails, the `max` rule will not be checked. Rules will be validated in the order they are assigned. - -#### A Note on Nested Attributes - -If the incoming HTTP request contains "nested" field data, you may specify these fields in your validation rules using "dot" syntax: - -```php -$request->validate([ - 'title' => 'required|unique:posts|max:255', - 'author.name' => 'required', - 'author.description' => 'required', -]); -``` - -On the other hand, if your field name contains a literal period, you can explicitly prevent this from being interpreted as "dot" syntax by escaping the period with a backslash: - -```php -$request->validate([ - 'title' => 'required|unique:posts|max:255', - 'v1\.0' => 'required', -]); -``` - -### Displaying the Validation Errors - -An `$errors` variable can be shared with all of your application's views by the `Hypervel\View\Middleware\ValidationExceptionHandle` middleware. You can add this middleware to the `web` middleware group manually. When this middleware is applied an `$errors` variable will always be available in your views, allowing you to conveniently assume the `$errors` variable is always defined and can be safely used. The `$errors` variable will be an instance of `Hyperf\Support\MessageBag`. For more information on working with this object, [check out its documentation](#working-with-error-messages). - -So, in our example, the user will be redirected to our controller's `create` method when validation fails, allowing us to display the error messages in the view: - -```blade - - -

      Create Post

      - -@if ($errors->any()) -
      -
        - @foreach ($errors->all() as $error) -
      • {{ $error }}
      • - @endforeach -
      -
      -@endif - - -``` - -#### Customizing the Error Messages - -Hypervel's built-in validation rules each have an error message that is located in your application's `lang/en/validation.php` file. If your application does not have a `lang` directory, you may instruct Hypervel to create it using the `lang:publish` Artisan command. - -Within the `lang/en/validation.php` file, you will find a translation entry for each validation rule. You are free to change or modify these messages based on the needs of your application. - -In addition, you may copy this file to another language directory to translate the messages for your application's language. To learn more about Hypervel localization, check out the complete [localization documentation](/docs/localization). - -#### XHR Requests and Validation - -In this example, we used a traditional form to send data to the application. However, many applications receive XHR requests from a JavaScript powered frontend. When using the `validate` method during an XHR request, Hypervel will not generate a redirect response. Instead, Hypervel generates a [JSON response containing all of the validation errors](#validation-error-response-format). This JSON response will be sent with a 422 HTTP status code. - -#### The `@error` Directive - -You may use the `@error` [Blade](/docs/blade) directive to quickly determine if validation error messages exist for a given attribute. Within an `@error` directive, you may echo the `$message` variable to display the error message: - -```blade - - - - - - -@error('title') -
      {{ $message }}
      -@enderror -``` - -If you are using [named error bags](#named-error-bags), you may pass the name of the error bag as the second argument to the `@error` directive: - -```blade - -``` - -### A Note on Optional Fields - -By default, Hypervel includes the `TrimStrings` and `ConvertEmptyStringsToNull` middleware in your application's global middleware stack. Because of this, you will often need to mark your "optional" request fields as `nullable` if you do not want the validator to consider `null` values as invalid. For example: - -```php -$request->validate([ - 'title' => 'required|unique:posts|max:255', - 'body' => 'required', - 'publish_at' => 'nullable|date', -]); -``` - -In this example, we are specifying that the `publish_at` field may be either `null` or a valid date representation. If the `nullable` modifier is not added to the rule definition, the validator would consider `null` an invalid date. - -### Validation Error Response Format - -When your application throws a `Hypervel\Validation\ValidationException` exception and the incoming HTTP request is expecting a JSON response, Hypervel will automatically format the error messages for you and return a `422 Unprocessable Entity` HTTP response. - -Below, you can review an example of the JSON response format for validation errors. Note that nested error keys are flattened into "dot" notation format: - -```json -{ - "message": "The team name must be a string. (and 4 more errors)", - "errors": { - "team_name": [ - "The team name must be a string.", - "The team name must be at least 1 characters." - ], - "authorization.role": [ - "The selected authorization.role is invalid." - ], - "users.0.email": [ - "The users.0.email field is required." - ], - "users.2.email": [ - "The users.2.email must be a valid email address." - ] - } -} -``` - -## Form Request Validation - -### Creating Form Requests - -For more complex validation scenarios, you may wish to create a "form request". Form requests are custom request classes that encapsulate their own validation and authorization logic. To create a form request class, you may use the `make:request` Artisan CLI command: - -```shell -php artisan make:request StorePostRequest -``` - -The generated form request class will be placed in the `app/Http/Requests` directory. If this directory does not exist, it will be created when you run the `make:request` command. Each form request generated by Hypervel has two methods: `authorize` and `rules`. - -As you might have guessed, the `authorize` method is responsible for determining if the currently authenticated user can perform the action represented by the request, while the `rules` method returns the validation rules that should apply to the request's data: - -```php -/** - * Get the validation rules that apply to the request. - * - * @return array|string> - */ -public function rules(): array -{ - return [ - 'title' => 'required|unique:posts|max:255', - 'body' => 'required', - ]; -} -``` - -::: note -You may type-hint any dependencies you require within the `rules` method's signature. They will automatically be resolved via the Hypervel [service container](/docs/container). -::: - -So, how are the validation rules evaluated? All you need to do is type-hint the request on your controller method. The incoming form request is validated before the controller method is called, meaning you do not need to clutter your controller with any validation logic: - -```php -/** - * Store a new blog post. - */ -public function store(StorePostRequest $request): ResponseInterface -{ - // The incoming request is valid... - - // Retrieve the validated input data... - $validated = $request->validated(); - - // Store the blog post... - - return redirect('/posts'); -} -``` - -If validation fails, a redirect response will be generated to send the user back to their previous location if `Hypervel\View\Middleware\ValidationExceptionHandle` middleware is applied. The errors will also be flashed to the session so they are available for display. If the request was an XHR request, an HTTP response with a 422 status code will be returned to the user including a [JSON representation of the validation errors](#validation-error-response-format). - - -#### Performing Additional Validation - -Sometimes you need to perform additional validation after your initial validation is complete. You can accomplish this using the form request's `after` method. - -The `after` method should return an array of callables or closures which will be invoked after validation is complete. The given callables will receive an `Hypervel\Validation\Validator` instance, allowing you to raise additional error messages if necessary: - -```php -use Hypervel\Validation\Validator; - -/** - * Get the "after" validation callables for the request. - */ -public function after(): array -{ - return [ - function (Validator $validator) { - if ($this->somethingElseIsInvalid()) { - $validator->errors()->add( - 'field', - 'Something is wrong with this field!' - ); - } - } - ]; -} -``` - -As noted, the array returned by the `after` method may also contain invokable classes. The `__invoke` method of these classes will receive an `Hypervel\Validation\Validator` instance: - -```php -use App\Validation\ValidateShippingTime; -use App\Validation\ValidateUserStatus; -use Hypervel\Validation\Validator; - -/** - * Get the "after" validation callables for the request. - */ -public function after(): array -{ - return [ - new ValidateUserStatus, - new ValidateShippingTime, - function (Validator $validator) { - // - } - ]; -} -``` - - -#### Stopping on the First Validation Failure - -By adding a `stopOnFirstFailure` property to your request class, you may inform the validator that it should stop validating all attributes once a single validation failure has occurred: - -```php -/** - * Indicates if the validator should stop on the first rule failure. - */ -protected bool $stopOnFirstFailure = true; -``` - -### Authorizing Form Requests - -The form request class also contains an `authorize` method. Within this method, you may determine if the authenticated user actually has the authority to update a given resource. For example, you may determine if a user actually owns a blog comment they are attempting to update. Most likely, you will interact with your [authorization gates and policies](/docs/authorization) within this method: - -```php -use App\Models\Comment; - -/** - * Determine if the user is authorized to make this request. - */ -public function authorize(): bool -{ - $comment = Comment::find($this->route('comment')); - - return $comment && $this->user()->can('update', $comment); -} -``` - -Since all form requests extend the base Hypervel request class, we may use the `user` method to access the currently authenticated user. Also, note the call to the `route` method in the example above. This method grants you access to the URI parameters defined on the route being called, such as the `{comment}` parameter in the example below: - -```php -Route::post('/comment/{comment}'); -``` - -Therefore, if your application is taking advantage of [route model binding](/docs/routing#route-model-binding), your code may be made even more succinct by accessing the resolved model as a property of the request: - -```php -return $this->user()->can('update', $this->comment); -``` - -If the `authorize` method returns `false`, an HTTP response with a 403 status code will automatically be returned and your controller method will not execute. - -If you plan to handle authorization logic for the request in another part of your application, you may remove the `authorize` method completely, or simply return `true`: - -```php -/** - * Determine if the user is authorized to make this request. - */ -public function authorize(): bool -{ - return true; -} -``` - -::: note -You may type-hint any dependencies you need within the `authorize` method's signature. They will automatically be resolved via the Hypervel [service container](/docs/container). -::: - -### Customizing the Error Messages - -You may customize the error messages used by the form request by overriding the `messages` method. This method should return an array of attribute / rule pairs and their corresponding error messages: - -```php -/** - * Get the error messages for the defined validation rules. - * - * @return array - */ -public function messages(): array -{ - return [ - 'title.required' => 'A title is required', - 'body.required' => 'A message is required', - ]; -} -``` - -#### Customizing the Validation Attributes - -Many of Hypervel's built-in validation rule error messages contain an `:attribute` placeholder. If you would like the `:attribute` placeholder of your validation message to be replaced with a custom attribute name, you may specify the custom names by overriding the `attributes` method. This method should return an array of attribute / name pairs: - -```php -/** - * Get custom attributes for validator errors. - * - * @return array - */ -public function attributes(): array -{ - return [ - 'email' => 'email address', - ]; -} -``` - -### Preparing Input for Validation - -If you need to prepare or sanitize any data from the request before you apply your validation rules, you may use the `prepareForValidation` method: - -```php -use Hypervel\Support\Str; - -/** - * Prepare the data for validation. - */ -protected function prepareForValidation(): void -{ - $this->merge([ - 'slug' => Str::slug($this->slug), - ]); -} -``` - -Likewise, if you need to normalize any request data after validation is complete, you may use the `passedValidation` method: - -```php -/** - * Handle a passed validation attempt. - */ -protected function passedValidation(): void -{ - $this->replace(['name' => 'Taylor']); -} -``` - -### Casting Form Request Data - -Hypervel's form requests include built-in support for automatic data casting, allowing you to transform request input to appropriate PHP types before or after validation. This feature is particularly useful for converting string inputs to the correct data types your application expects. - -#### Defining Casts - -You can define how request data should be cast by adding a `casts` property to your form request class or by implementing a `casts()` method: - -```php - 'datetime', - 'views' => 'integer', - 'is_featured' => 'boolean', - 'tags' => 'array', - 'metadata' => 'json', - 'rating' => 'decimal:2', - ]; - - public function rules(): array - { - return [ - 'title' => 'required|string|max:255', - 'published_at' => 'required|date', - 'views' => 'required|integer|min:0', - 'is_featured' => 'required|boolean', - 'tags' => 'required|array', - 'metadata' => 'required|json', - 'rating' => 'required|numeric|between:0,5', - ]; - } -} -``` - -Alternatively, you can define casts using a method, which is useful for dynamic casting: - -```php -/** - * Get the casts for the form request. - */ -protected function casts(): array -{ - return [ - 'price' => 'decimal:' . $this->input('currency_decimals', 2), - 'settings' => 'json', - ]; -} -``` - -#### Available Cast Types - -Hypervel supports the following primitive cast types: - -
      - -- `array` - Casts to PHP array -- `bool` / `boolean` - Casts to boolean -- `collection` - Casts to Hypervel Collection -- `date` - Casts to Carbon date (time set to 00:00:00) -- `datetime` - Casts to Carbon datetime -- `decimal:digits` - Casts to string with specified decimal places -- `double` / `float` / `real` - Casts to float -- `int` / `integer` - Casts to integer -- `json` - Decodes JSON string to array -- `object` - Decodes JSON string to object -- `string` - Casts to string -- `timestamp` - Casts to Unix timestamp - -
      - -#### Retrieving Casted Data - -After defining your casts, you can retrieve the casted data using the `casted()` method: - -```php -public function store(CreatePostRequest $request) -{ - // Get all casted data - $data = $request->casted(); - - // $data['published_at'] is now a Carbon instance - // $data['views'] is now an integer - // $data['is_featured'] is now a boolean - // $data['tags'] is now an array - - Post::create($data); -} -``` - -You can also retrieve specific casted values: - -```php -// Get a single casted value -$publishedAt = $request->casted('published_at'); - -// Get multiple casted values -$data = $request->casted(['published_at', 'views', 'is_featured']); -``` - -#### Casting Validation vs Raw Input - -By default, the `casted()` method works with validated data. You can also cast raw input data by passing `false` as the second parameter: - -```php -// Cast validated data (default) -$validatedData = $request->casted(); - -// Cast raw input data -$rawData = $request->casted(null, false); -``` - -#### Enum Casting - -You can cast request input to PHP enums: - -```php - App\Enums\PostStatus::class, -]; - -public function store(CreatePostRequest $request) -{ - $data = $request->casted(); - // $data['status'] is now a PostStatus enum instance -} -``` - -#### DataObject Casting - -You can cast arrays to custom DataObject classes: - -```php - App\DataObjects\PostMetadata::class, -]; - -public function store(CreatePostRequest $request) -{ - $data = $request->casted(); - // $data['metadata'] is now a PostMetadata instance -} -``` - -#### Custom Cast Classes - -For more complex casting logic, you can create custom cast classes that implement the `CastInputs` interface: - -```php - App\Casts\UppercaseCast::class, -]; -``` - -#### Castable Classes - -You can also create "castable" classes that define their own casting logic: - -```php - App\DataObjects\Money::class, -]; -``` - -::: note -Form request casting provides a clean way to ensure your request data is in the correct format before it reaches your application logic, reducing the need for manual type conversion throughout your codebase. -::: - -## Manually Creating Validators - -If you do not want to use the `validate` method on the request, you may create a validator instance manually using the `Validator` [facade](/docs/facades). The `make` method on the facade generates a new validator instance: - -```php -all(), [ - 'title' => 'required|unique:posts|max:255', - 'body' => 'required', - ]); - - if ($validator->fails()) { - return redirect('post/create'); - } - - // Retrieve the validated input... - $validated = $validator->validated(); - - // Store the blog post... - - return redirect('/posts'); - } -} -``` - -The first argument passed to the `make` method is the data under validation. The second argument is an array of the validation rules that should be applied to the data. - -#### Stopping on First Validation Failure - -The `stopOnFirstFailure` method will inform the validator that it should stop validating all attributes once a single validation failure has occurred: - -```php -if ($validator->stopOnFirstFailure()->fails()) { - // ... -} -``` - -### Automatic Redirection - -If you would like to create a validator instance manually but still take advantage of the automatic redirection offered by the HTTP request's `validate` method, you may call the `validate` method on an existing validator instance. If validation fails, the user will automatically be redirected or, in the case of an XHR request, a [JSON response will be returned](#validation-error-response-format): - -```php -Validator::make($request->all(), [ - 'title' => 'required|unique:posts|max:255', - 'body' => 'required', -])->validate(); -``` - -You may use the `validateWithBag` method to store the error messages in a [named error bag](#named-error-bags) if validation fails: - -```php -Validator::make($request->all(), [ - 'title' => 'required|unique:posts|max:255', - 'body' => 'required', -])->validateWithBag('post'); -``` - - -### Customizing the Error Messages - -If needed, you may provide custom error messages that a validator instance should use instead of the default error messages provided by Hypervel. There are several ways to specify custom messages. First, you may pass the custom messages as the third argument to the `Validator::make` method: - -```php -$validator = Validator::make($input, $rules, $messages = [ - 'required' => 'The :attribute field is required.', -]); -``` - -In this example, the `:attribute` placeholder will be replaced by the actual name of the field under validation. You may also utilize other placeholders in validation messages. For example: - -```php -$messages = [ - 'same' => 'The :attribute and :other must match.', - 'size' => 'The :attribute must be exactly :size.', - 'between' => 'The :attribute value :input is not between :min - :max.', - 'in' => 'The :attribute must be one of the following types: :values', -]; -``` - -#### Specifying a Custom Message for a Given Attribute - -Sometimes you may wish to specify a custom error message only for a specific attribute. You may do so using "dot" notation. Specify the attribute's name first, followed by the rule: - -```php -$messages = [ - 'email.required' => 'We need to know your email address!', -]; -``` - -#### Specifying Custom Attribute Values - -Many of Hypervel's built-in error messages include an `:attribute` placeholder that is replaced with the name of the field or attribute under validation. To customize the values used to replace these placeholders for specific fields, you may pass an array of custom attributes as the fourth argument to the `Validator::make` method: - -```php -$validator = Validator::make($input, $rules, $messages, [ - 'email' => 'email address', -]); -``` - -### Performing Additional Validation - -Sometimes you need to perform additional validation after your initial validation is complete. You can accomplish this using the validator's `after` method. The `after` method accepts a closure or an array of callables which will be invoked after validation is complete. The given callables will receive an `Hypervel\Validation\Validator` instance, allowing you to raise additional error messages if necessary: - -```php -use Hypervel\Support\Facades\Validator; - -$validator = Validator::make(/* ... */); - -$validator->after(function ($validator) { - if ($this->somethingElseIsInvalid()) { - $validator->errors()->add( - 'field', 'Something is wrong with this field!' - ); - } -}); - -if ($validator->fails()) { - // ... -} -``` - -As noted, the `after` method also accepts an array of callables, which is particularly convenient if your "after validation" logic is encapsulated in invokable classes, which will receive an `Hypervel\Validation\Validator` instance via their `__invoke` method: - -```php -use App\Validation\ValidateShippingTime; -use App\Validation\ValidateUserStatus; - -$validator->after([ - new ValidateUserStatus, - new ValidateShippingTime, - function ($validator) { - // ... - }, -]); -``` - -## Working With Validated Input - -After validating incoming request data using a form request or a manually created validator instance, you may wish to retrieve the incoming request data that actually underwent validation. This can be accomplished in several ways. First, you may call the `validated` method on a form request or validator instance. This method returns an array of the data that was validated: - -```php -$validated = $request->validated(); - -$validated = $validator->validated(); -``` - -## Working With Error Messages - -After calling the `errors` method on a `Validator` instance, you will receive an `Hyperf\Support\MessageBag` instance, which has a variety of convenient methods for working with error messages. The `$errors` variable that is automatically made available to all views is also an instance of the `MessageBag` class. - -#### Retrieving the First Error Message for a Field - -To retrieve the first error message for a given field, use the `first` method: - -```php -$errors = $validator->errors(); - -echo $errors->first('email'); -``` - -#### Retrieving All Error Messages for a Field - -If you need to retrieve an array of all the messages for a given field, use the `get` method: - -```php -foreach ($errors->get('email') as $message) { - // ... -} -``` - -If you are validating an array form field, you may retrieve all of the messages for each of the array elements using the `*` character: - -```php -foreach ($errors->get('attachments.*') as $message) { - // ... -} -``` - -#### Retrieving All Error Messages for All Fields - -To retrieve an array of all messages for all fields, use the `all` method: - -```php -foreach ($errors->all() as $message) { - // ... -} -``` - -#### Determining if Messages Exist for a Field - -The `has` method may be used to determine if any error messages exist for a given field: - -```php -if ($errors->has('email')) { - // ... -} -``` - -### Specifying Custom Messages in Language Files - -Hypervel's built-in validation rules each have an error message that is located in your application's `lang/en/validation.php` file. - -Within the `lang/en/validation.php` file, you will find a translation entry for each validation rule. You are free to change or modify these messages based on the needs of your application. - -In addition, you may copy this file to another language directory to translate the messages for your application's language. To learn more about Hypervel localization, check out the complete [localization documentation](/docs/localization). - -#### Custom Messages for Specific Attributes - -You may customize the error messages used for specified attribute and rule combinations within your application's validation language files. To do so, add your message customizations to the `custom` array of your application's `lang/xx/validation.php` language file: - -```php -'custom' => [ - 'email' => [ - 'required' => 'We need to know your email address!', - 'max' => 'Your email address is too long!' - ], -], -``` - -### Specifying Attributes in Language Files - -Many of Hypervel's built-in error messages include an `:attribute` placeholder that is replaced with the name of the field or attribute under validation. If you would like the `:attribute` portion of your validation message to be replaced with a custom value, you may specify the custom attribute name in the `attributes` array of your `lang/xx/validation.php` language file: - -```php -'attributes' => [ - 'email' => 'email address', -], -``` - -### Specifying Values in Language Files - -Some of Hypervel's built-in validation rule error messages contain a `:value` placeholder that is replaced with the current value of the request attribute. However, you may occasionally need the `:value` portion of your validation message to be replaced with a custom representation of the value. For example, consider the following rule that specifies that a credit card number is required if the `payment_type` has a value of `cc`: - -```php -Validator::make($request->all(), [ - 'credit_card_number' => 'required_if:payment_type,cc' -]); -``` - -If this validation rule fails, it will produce the following error message: - -```text -The credit card number field is required when payment type is cc. -``` - -Instead of displaying `cc` as the payment type value, you may specify a more user-friendly value representation in your `lang/xx/validation.php` language file by defining a `values` array: - -```php -'values' => [ - 'payment_type' => [ - 'cc' => 'credit card' - ], -], -``` - -After defining this value, the validation rule will produce the following error message: - -```text -The credit card number field is required when payment type is credit card. -``` - -## Available Validation Rules - -Below is a list of all available validation rules and their function: - - - -#### Booleans - -
      - -[Accepted](#rule-accepted) -[Accepted If](#rule-accepted-if) -[Boolean](#rule-boolean) -[Declined](#rule-declined) -[Declined If](#rule-declined-if) - -
      - -#### Strings - -
      - -[Active URL](#rule-active-url) -[Alpha](#rule-alpha) -[Alpha Dash](#rule-alpha-dash) -[Alpha Numeric](#rule-alpha-num) -[Ascii](#rule-ascii) -[Confirmed](#rule-confirmed) -[Current Password](#rule-current-password) -[Different](#rule-different) -[Doesnt Start With](#rule-doesnt-start-with) -[Doesnt End With](#rule-doesnt-end-with) -[Email](#rule-email) -[Ends With](#rule-ends-with) -[Enum](#rule-enum) -[Hex Color](#rule-hex-color) -[In](#rule-in) -[IP Address](#rule-ip) -[JSON](#rule-json) -[Lowercase](#rule-lowercase) -[MAC Address](#rule-mac) -[Max](#rule-max) -[Min](#rule-min) -[Not In](#rule-not-in) -[Regular Expression](#rule-regex) -[Not Regular Expression](#rule-not-regex) -[Same](#rule-same) -[Size](#rule-size) -[Starts With](#rule-starts-with) -[String](#rule-string) -[Uppercase](#rule-uppercase) -[URL](#rule-url) -[ULID](#rule-ulid) -[UUID](#rule-uuid) - -
      - -#### Numbers - -
      - -[Between](#rule-between) -[Decimal](#rule-decimal) -[Different](#rule-different) -[Digits](#rule-digits) -[Digits Between](#rule-digits-between) -[Greater Than](#rule-gt) -[Greater Than Or Equal](#rule-gte) -[Integer](#rule-integer) -[Less Than](#rule-lt) -[Less Than Or Equal](#rule-lte) -[Max](#rule-max) -[Max Digits](#rule-max-digits) -[Min](#rule-min) -[Min Digits](#rule-min-digits) -[Multiple Of](#rule-multiple-of) -[Numeric](#rule-numeric) -[Same](#rule-same) -[Size](#rule-size) - -
      - -#### Arrays - -
      - -[Array](#rule-array) -[Between](#rule-between) -[Contains](#rule-contains) -[Distinct](#rule-distinct) -[In Array](#rule-in-array) -[List](#rule-list) -[Max](#rule-max) -[Min](#rule-min) -[Size](#rule-size) - -
      - -#### Dates - -
      - -[After](#rule-after) -[After Or Equal](#rule-after-or-equal) -[Before](#rule-before) -[Before Or Equal](#rule-before-or-equal) -[Date](#rule-date) -[Date Equals](#rule-date-equals) -[Date Format](#rule-date-format) -[Different](#rule-different) -[Timezone](#rule-timezone) - -
      - -#### Files - -
      - -[Between](#rule-between) -[Dimensions](#rule-dimensions) -[Extensions](#rule-extensions) -[File](#rule-file) -[Image](#rule-image) -[Max](#rule-max) -[MIME Types](#rule-mimetypes) -[MIME Type By File Extension](#rule-mimes) -[Size](#rule-size) - -
      - -#### Database - -
      - -[Exists](#rule-exists) -[Unique](#rule-unique) - -
      - -#### Utilities - -
      - -[Any Of](#rule-anyof) -[Bail](#rule-bail) -[Exclude](#rule-exclude) -[Exclude If](#rule-exclude-if) -[Exclude Unless](#rule-exclude-unless) -[Exclude With](#rule-exclude-with) -[Exclude Without](#rule-exclude-without) -[Filled](#rule-filled) -[Missing](#rule-missing) -[Missing If](#rule-missing-if) -[Missing Unless](#rule-missing-unless) -[Missing With](#rule-missing-with) -[Missing With All](#rule-missing-with-all) -[Nullable](#rule-nullable) -[Present](#rule-present) -[Present If](#rule-present-if) -[Present Unless](#rule-present-unless) -[Present With](#rule-present-with) -[Present With All](#rule-present-with-all) -[Prohibited](#rule-prohibited) -[Prohibited If](#rule-prohibited-if) -[Prohibited If Accepted](#rule-prohibited-if-accepted) -[Prohibited If Declined](#rule-prohibited-if-declined) -[Prohibited Unless](#rule-prohibited-unless) -[Prohibits](#rule-prohibits) -[Required](#rule-required) -[Required If](#rule-required-if) -[Required If Accepted](#rule-required-if-accepted) -[Required If Declined](#rule-required-if-declined) -[Required Unless](#rule-required-unless) -[Required With](#rule-required-with) -[Required With All](#rule-required-with-all) -[Required Without](#rule-required-without) -[Required Without All](#rule-required-without-all) -[Required Array Keys](#rule-required-array-keys) -[Sometimes](#validating-when-present) - -
      - - -#### accepted - -The field under validation must be `"yes"`, `"on"`, `1`, `"1"`, `true`, or `"true"`. This is useful for validating "Terms of Service" acceptance or similar fields. - - -#### accepted_if:anotherfield,value,... - -The field under validation must be `"yes"`, `"on"`, `1`, `"1"`, `true`, or `"true"` if another field under validation is equal to a specified value. This is useful for validating "Terms of Service" acceptance or similar fields. - - -#### active_url - -The field under validation must have a valid A or AAAA record according to the `dns_get_record` PHP function. The hostname of the provided URL is extracted using the `parse_url` PHP function before being passed to `dns_get_record`. - - -#### after:_date_ - -The field under validation must be a value after a given date. The dates will be passed into the `strtotime` PHP function in order to be converted to a valid `DateTime` instance: - -```php -'start_date' => 'required|date|after:tomorrow' -``` - -Instead of passing a date string to be evaluated by `strtotime`, you may specify another field to compare against the date: - -```php -'finish_date' => 'required|date|after:start_date' -``` - -For convenience, date based rules may be constructed using the fluent `date` rule builder: - -```php -use Hypervel\Validation\Rule; - -'start_date' => [ - 'required', - Rule::date()->after(today()->addDays(7)), -], -``` - -The `afterToday` and `todayOrAfter` methods may be used to fluently express the date must be after today or or today or after, respectively: - -```php -'start_date' => [ - 'required', - Rule::date()->afterToday(), -], -``` - - -#### after\_or\_equal:_date_ - -The field under validation must be a value after or equal to the given date. For more information, see the [after](#rule-after) rule. - -For convenience, date based rules may be constructed using the fluent `date` rule builder: - -```php -use Hypervel\Validation\Rule; - -'start_date' => [ - 'required', - Rule::date()->afterOrEqual(today()->addDays(7)), -], -``` - - -#### anyOf - -The `Rule::anyOf` validation rule allows you to specify that the field under validation must satisfy any of the given validation rulesets. For example, the following rule will validate that the `username` field is either an email address or an alpha-numeric string (including dashes) that is at least 6 characters long: - -```php -use Hypervel\Validation\Rule; - -'username' => [ - 'required', - Rule::anyOf([ - ['string', 'email'], - ['string', 'alpha_dash', 'min:6'], - ]), -], -``` - - -#### alpha - -The field under validation must be entirely Unicode alphabetic characters contained in [\p{L}](https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AL%3A%5D&g=&i=) and [\p{M}](https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AM%3A%5D&g=&i=). - -To restrict this validation rule to characters in the ASCII range (`a-z` and `A-Z`), you may provide the `ascii` option to the validation rule: - -```php -'username' => 'alpha:ascii', -``` - - -#### alpha_dash - -The field under validation must be entirely Unicode alpha-numeric characters contained in [\p{L}](https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AL%3A%5D&g=&i=), [\p{M}](https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AM%3A%5D&g=&i=), [\p{N}](https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AN%3A%5D&g=&i=), as well as ASCII dashes (`-`) and ASCII underscores (`_`). - -To restrict this validation rule to characters in the ASCII range (`a-z`, `A-Z`, and `0-9`), you may provide the `ascii` option to the validation rule: - -```php -'username' => 'alpha_dash:ascii', -``` - - -#### alpha_num - -The field under validation must be entirely Unicode alpha-numeric characters contained in [\p{L}](https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AL%3A%5D&g=&i=), [\p{M}](https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AM%3A%5D&g=&i=), and [\p{N}](https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5B%3AN%3A%5D&g=&i=). - -To restrict this validation rule to characters in the ASCII range (`a-z`, `A-Z`, and `0-9`), you may provide the `ascii` option to the validation rule: - -```php -'username' => 'alpha_num:ascii', -``` - - -#### array - -The field under validation must be a PHP `array`. - -When additional values are provided to the `array` rule, each key in the input array must be present within the list of values provided to the rule. In the following example, the `admin` key in the input array is invalid since it is not contained in the list of values provided to the `array` rule: - -```php -use Hypervel\Support\Facades\Validator; - -$input = [ - 'user' => [ - 'name' => 'Taylor Otwell', - 'username' => 'taylorotwell', - 'admin' => true, - ], -]; - -Validator::make($input, [ - 'user' => 'array:name,username', -]); -``` - -In general, you should always specify the array keys that are allowed to be present within your array. - - -#### ascii - -The field under validation must be entirely 7-bit ASCII characters. - - -#### bail - -Stop running validation rules for the field after the first validation failure. - -While the `bail` rule will only stop validating a specific field when it encounters a validation failure, the `stopOnFirstFailure` method will inform the validator that it should stop validating all attributes once a single validation failure has occurred: - -```php -if ($validator->stopOnFirstFailure()->fails()) { - // ... -} -``` - - -#### before:_date_ - -The field under validation must be a value preceding the given date. The dates will be passed into the PHP `strtotime` function in order to be converted into a valid `DateTime` instance. In addition, like the [after](#rule-after) rule, the name of another field under validation may be supplied as the value of `date`. - -For convenience, date based rules may also be constructed using the fluent `date` rule builder: - -```php -use Hypervel\Validation\Rule; - -'start_date' => [ - 'required', - Rule::date()->before(today()->subDays(7)), -], -``` - -The `beforeToday` and `todayOrBefore` methods may be used to fluently express the date must be before today or or today or before, respectively: - -```php -'start_date' => [ - 'required', - Rule::date()->beforeToday(), -], -``` - - -#### before\_or\_equal:_date_ - -The field under validation must be a value preceding or equal to the given date. The dates will be passed into the PHP `strtotime` function in order to be converted into a valid `DateTime` instance. In addition, like the [after](#rule-after) rule, the name of another field under validation may be supplied as the value of `date`. - -For convenience, date based rules may also be constructed using the fluent `date` rule builder: - -```php -use Hypervel\Validation\Rule; - -'start_date' => [ - 'required', - Rule::date()->beforeOrEqual(today()->subDays(7)), -], -``` - - -#### between:_min_,_max_ - -The field under validation must have a size between the given _min_ and _max_ (inclusive). Strings, numerics, arrays, and files are evaluated in the same fashion as the [size](#rule-size) rule. - - -#### boolean - -The field under validation must be able to be cast as a boolean. Accepted input are `true`, `false`, `1`, `0`, `"1"`, and `"0"`. - - -#### confirmed - -The field under validation must have a matching field of `{field}_confirmation`. For example, if the field under validation is `password`, a matching `password_confirmation` field must be present in the input. - -You may also pass a custom confirmation field name. For example, `confirmed:repeat_username` will expect the field `repeat_username` to match the field under validation. - - -#### contains:_foo_,_bar_,... - -The field under validation must be an array that contains all of the given parameter values. - - -#### current_password - -The field under validation must match the authenticated user's password. You may specify an [authentication guard](/docs/authentication) using the rule's first parameter: - -```php -'password' => 'current_password:api' -``` - - -#### date - -The field under validation must be a valid, non-relative date according to the `strtotime` PHP function. - - -#### date_equals:_date_ - -The field under validation must be equal to the given date. The dates will be passed into the PHP `strtotime` function in order to be converted into a valid `DateTime` instance. - - -#### date_format:_format_,... - -The field under validation must match one of the given _formats_. You should use **either** `date` or `date_format` when validating a field, not both. This validation rule supports all formats supported by PHP's [DateTime](https://www.php.net/manual/en/class.datetime.php) class. - -For convenience, date based rules may be constructed using the fluent `date` rule builder: - -```php -use Hypervel\Validation\Rule; - -'start_date' => [ - 'required', - Rule::date()->format('Y-m-d'), -], -``` - - -#### decimal:_min_,_max_ - -The field under validation must be numeric and must contain the specified number of decimal places: - -```php -// Must have exactly two decimal places (9.99)... -'price' => 'decimal:2' - -// Must have between 2 and 4 decimal places... -'price' => 'decimal:2,4' -``` - - -#### declined - -The field under validation must be `"no"`, `"off"`, `0`, `"0"`, `false`, or `"false"`. - - -#### declined_if:anotherfield,value,... - -The field under validation must be `"no"`, `"off"`, `0`, `"0"`, `false`, or `"false"` if another field under validation is equal to a specified value. - - -#### different:_field_ - -The field under validation must have a different value than _field_. - - -#### digits:_value_ - -The integer under validation must have an exact length of _value_. - - -#### digits_between:_min_,_max_ - -The integer validation must have a length between the given _min_ and _max_. - - -#### dimensions - -The file under validation must be an image meeting the dimension constraints as specified by the rule's parameters: - -```php -'avatar' => 'dimensions:min_width=100,min_height=200' -``` - -Available constraints are: _min\_width_, _max\_width_, _min\_height_, _max\_height_, _width_, _height_, _ratio_. - -A _ratio_ constraint should be represented as width divided by height. This can be specified either by a fraction like `3/2` or a float like `1.5`: - -```php -'avatar' => 'dimensions:ratio=3/2' -``` - -Since this rule requires several arguments, it is often more convenient to use the `Rule::dimensions` method to fluently construct the rule: - -```php -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rule; - -Validator::make($data, [ - 'avatar' => [ - 'required', - Rule::dimensions() - ->maxWidth(1000) - ->maxHeight(500) - ->ratio(3 / 2), - ], -]); -``` - - -#### distinct - -When validating arrays, the field under validation must not have any duplicate values: - -```php -'foo.*.id' => 'distinct' -``` - -Distinct uses loose variable comparisons by default. To use strict comparisons, you may add the `strict` parameter to your validation rule definition: - -```php -'foo.*.id' => 'distinct:strict' -``` - -You may add `ignore_case` to the validation rule's arguments to make the rule ignore capitalization differences: - -```php -'foo.*.id' => 'distinct:ignore_case' -``` - - -#### doesnt_start_with:_foo_,_bar_,... - -The field under validation must not start with one of the given values. - - -#### doesnt_end_with:_foo_,_bar_,... - -The field under validation must not end with one of the given values. - - -#### email - -The field under validation must be formatted as an email address. This validation rule utilizes the [egulias/email-validator](https://github.com/egulias/EmailValidator) package for validating the email address. By default, the `RFCValidation` validator is applied, but you can apply other validation styles as well: - -```php -'email' => 'email:rfc,dns' -``` - -The example above will apply the `RFCValidation` and `DNSCheckValidation` validations. Here's a full list of validation styles you can apply: - -
      - -- `rfc`: `RFCValidation` - Validate the email address according to [supported RFCs](https://github.com/egulias/EmailValidator?tab=readme-ov-file#supported-rfcs). -- `strict`: `NoRFCWarningsValidation` - Validate the email according to [supported RFCs](https://github.com/egulias/EmailValidator?tab=readme-ov-file#supported-rfcs), failing when warnings are found (e.g. trailing periods and multiple consecutive periods). -- `dns`: `DNSCheckValidation` - Ensure the email address's domain has a valid MX record. -- `spoof`: `SpoofCheckValidation` - Ensure the email address does not contain homograph or deceptive Unicode characters. -- `filter`: `FilterEmailValidation` - Ensure the email address is valid according to PHP's `filter_var` function. -- `filter_unicode`: `FilterEmailValidation::unicode()` - Ensure the email address is valid according to PHP's `filter_var` function, allowing some Unicode characters. - -
      - -For convenience, email validation rules may be built using the fluent rule builder: - -```php -use Hypervel\Validation\Rule; - -$request->validate([ - 'email' => [ - 'required', - Rule::email() - ->rfcCompliant(strict: false) - ->validateMxRecord() - ->preventSpoofing() - ], -]); -``` - -::: warning -The `dns` and `spoof` validators require the PHP `intl` extension. -::: - - -#### ends_with:_foo_,_bar_,... - -The field under validation must end with one of the given values. - - -#### enum - -The `Enum` rule is a class based rule that validates whether the field under validation contains a valid enum value. The `Enum` rule accepts the name of the enum as its only constructor argument. When validating primitive values, a backed Enum should be provided to the `Enum` rule: - -```php -use App\Enums\ServerStatus; -use Hypervel\Validation\Rule; - -$request->validate([ - 'status' => [Rule::enum(ServerStatus::class)], -]); -``` - -The `Enum` rule's `only` and `except` methods may be used to limit which enum cases should be considered valid: - -```php -Rule::enum(ServerStatus::class) - ->only([ServerStatus::Pending, ServerStatus::Active]); - -Rule::enum(ServerStatus::class) - ->except([ServerStatus::Pending, ServerStatus::Active]); -``` - -The `when` method may be used to conditionally modify the `Enum` rule: - -```php -use Hypervel\Support\Facades\Auth; -use Hypervel\Validation\Rule; - -Rule::enum(ServerStatus::class) - ->when( - Auth::user()->isAdmin(), - fn ($rule) => $rule->only(...), - fn ($rule) => $rule->only(...), - ); -``` - - -#### exclude - -The field under validation will be excluded from the request data returned by the `validate` and `validated` methods. - - -#### exclude_if:_anotherfield_,_value_ - -The field under validation will be excluded from the request data returned by the `validate` and `validated` methods if the _anotherfield_ field is equal to _value_. - -If complex conditional exclusion logic is required, you may utilize the `Rule::excludeIf` method. This method accepts a boolean or a closure. When given a closure, the closure should return `true` or `false` to indicate if the field under validation should be excluded: - -```php -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rule; - -Validator::make($request->all(), [ - 'role_id' => Rule::excludeIf($request->user()->is_admin), -]); - -Validator::make($request->all(), [ - 'role_id' => Rule::excludeIf(fn () => $request->user()->is_admin), -]); -``` - - -#### exclude_unless:_anotherfield_,_value_ - -The field under validation will be excluded from the request data returned by the `validate` and `validated` methods unless _anotherfield_'s field is equal to _value_. If _value_ is `null` (`exclude_unless:name,null`), the field under validation will be excluded unless the comparison field is `null` or the comparison field is missing from the request data. - - -#### exclude_with:_anotherfield_ - -The field under validation will be excluded from the request data returned by the `validate` and `validated` methods if the _anotherfield_ field is present. - - -#### exclude_without:_anotherfield_ - -The field under validation will be excluded from the request data returned by the `validate` and `validated` methods if the _anotherfield_ field is not present. - - -#### exists:_table_,_column_ - -The field under validation must exist in a given database table. - - -#### Basic Usage of Exists Rule - -```php -'state' => 'exists:states' -``` - -If the `column` option is not specified, the field name will be used. So, in this case, the rule will validate that the `states` database table contains a record with a `state` column value matching the request's `state` attribute value. - - -#### Specifying a Custom Column Name - -You may explicitly specify the database column name that should be used by the validation rule by placing it after the database table name: - -```php -'state' => 'exists:states,abbreviation' -``` - -Occasionally, you may need to specify a specific database connection to be used for the `exists` query. You can accomplish this by prepending the connection name to the table name: - -```php -'email' => 'exists:connection.staff,email' -``` - -Instead of specifying the table name directly, you may specify the Eloquent model which should be used to determine the table name: - -```php -'user_id' => 'exists:App\Models\User,id' -``` - -If you would like to customize the query executed by the validation rule, you may use the `Rule` class to fluently define the rule. In this example, we'll also specify the validation rules as an array instead of using the `|` character to delimit them: - -```php -use Hyperf\Database\Query\Builder; -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rule; - -Validator::make($data, [ - 'email' => [ - 'required', - Rule::exists('staff')->where(function (Builder $query) { - $query->where('account_id', 1); - }), - ], -]); -``` - -You may explicitly specify the database column name that should be used by the `exists` rule generated by the `Rule::exists` method by providing the column name as the second argument to the `exists` method: - -```php -'state' => Rule::exists('states', 'abbreviation'), -``` - -Sometimes, you may wish to validate whether an array of values exists in the database. You can do so by adding both the `exists` and [array](#rule-array) rules to the field being validated: - -```php -'states' => ['array', Rule::exists('states', 'abbreviation')], -``` - -When both of these rules are assigned to a field, Hypervel will automatically build a single query to determine if all of the given values exist in the specified table. - - -#### extensions:_foo_,_bar_,... - -The file under validation must have a user-assigned extension corresponding to one of the listed extensions: - -```php -'photo' => ['required', 'extensions:jpg,png'], -``` - -::: warning -You should never rely on validating a file by its user-assigned extension alone. This rule should typically always be used in combination with the [mimes](#rule-mimes) or [mimetypes](#rule-mimetypes) rules. -::: - - -#### file - -The field under validation must be a successfully uploaded file. - - -#### filled - -The field under validation must not be empty when it is present. - - -#### gt:_field_ - -The field under validation must be greater than the given _field_ or _value_. The two fields must be of the same type. Strings, numerics, arrays, and files are evaluated using the same conventions as the [size](#rule-size) rule. - - -#### gte:_field_ - -The field under validation must be greater than or equal to the given _field_ or _value_. The two fields must be of the same type. Strings, numerics, arrays, and files are evaluated using the same conventions as the [size](#rule-size) rule. - - -#### hex_color - -The field under validation must contain a valid color value in [hexadecimal](https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color) format. - - -#### image - -The file under validation must be an image (jpg, jpeg, png, bmp, gif, or webp). - -::: warning -By default, the image rule does not allow SVG files due to the possibility of XSS vulnerabilities. If you need to allow SVG files, you may provide the `allow_svg` directive to the `image` rule (`image:allow_svg`). -::: - - -#### in:_foo_,_bar_,... - -The field under validation must be included in the given list of values. Since this rule often requires you to `implode` an array, the `Rule::in` method may be used to fluently construct the rule: - -```php -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rule; - -Validator::make($data, [ - 'zones' => [ - 'required', - Rule::in(['first-zone', 'second-zone']), - ], -]); -``` - -When the `in` rule is combined with the `array` rule, each value in the input array must be present within the list of values provided to the `in` rule. In the following example, the `LAS` airport code in the input array is invalid since it is not contained in the list of airports provided to the `in` rule: - -```php -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rule; - -$input = [ - 'airports' => ['NYC', 'LAS'], -]; - -Validator::make($input, [ - 'airports' => [ - 'required', - 'array', - ], - 'airports.*' => Rule::in(['NYC', 'LIT']), -]); -``` - - -#### in_array:_anotherfield_.* - -The field under validation must exist in _anotherfield_'s values. - - -#### integer - -The field under validation must be an integer. - -::: warning -This validation rule does not verify that the input is of the "integer" variable type, only that the input is of a type accepted by PHP's `FILTER_VALIDATE_INT` rule. If you need to validate the input as being a number please use this rule in combination with [the `numeric` validation rule](#rule-numeric). -::: - - -#### ip - -The field under validation must be an IP address. - - -#### ipv4 - -The field under validation must be an IPv4 address. - - -#### ipv6 - -The field under validation must be an IPv6 address. - - -#### json - -The field under validation must be a valid JSON string. - - -#### lt:_field_ - -The field under validation must be less than the given _field_. The two fields must be of the same type. Strings, numerics, arrays, and files are evaluated using the same conventions as the [size](#rule-size) rule. - - -#### lte:_field_ - -The field under validation must be less than or equal to the given _field_. The two fields must be of the same type. Strings, numerics, arrays, and files are evaluated using the same conventions as the [size](#rule-size) rule. - - -#### lowercase - -The field under validation must be lowercase. - - -#### list - -The field under validation must be an array that is a list. An array is considered a list if its keys consist of consecutive numbers from 0 to `count($array) - 1`. - - -#### mac_address - -The field under validation must be a MAC address. - - -#### max:_value_ - -The field under validation must be less than or equal to a maximum _value_. Strings, numerics, arrays, and files are evaluated in the same fashion as the [size](#rule-size) rule. - - -#### max_digits:_value_ - -The integer under validation must have a maximum length of _value_. - - -#### mimetypes:_text/plain_,... - -The file under validation must match one of the given MIME types: - -```php -'video' => 'mimetypes:video/avi,video/mpeg,video/quicktime' -``` - -To determine the MIME type of the uploaded file, the file's contents will be read and the framework will attempt to guess the MIME type, which may be different from the client's provided MIME type. - - -#### mimes:_foo_,_bar_,... - -The file under validation must have a MIME type corresponding to one of the listed extensions: - -```php -'photo' => 'mimes:jpg,bmp,png' -``` - -Even though you only need to specify the extensions, this rule actually validates the MIME type of the file by reading the file's contents and guessing its MIME type. A full listing of MIME types and their corresponding extensions may be found at the following location: - -[https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types](https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types) - -#### MIME Types and Extensions - -This validation rule does not verify agreement between the MIME type and the extension the user assigned to the file. For example, the `mimes:png` validation rule would consider a file containing valid PNG content to be a valid PNG image, even if the file is named `photo.txt`. If you would like to validate the user-assigned extension of the file, you may use the [extensions](#rule-extensions) rule. - - -#### min:_value_ - -The field under validation must have a minimum _value_. Strings, numerics, arrays, and files are evaluated in the same fashion as the [size](#rule-size) rule. - - -#### min_digits:_value_ - -The integer under validation must have a minimum length of _value_. - - -#### multiple_of:_value_ - -The field under validation must be a multiple of _value_. - - -#### missing - -The field under validation must not be present in the input data. - - -#### missing_if:_anotherfield_,_value_,... - -The field under validation must not be present if the _anotherfield_ field is equal to any _value_. - - -#### missing_unless:_anotherfield_,_value_ - -The field under validation must not be present unless the _anotherfield_ field is equal to any _value_. - - -#### missing_with:_foo_,_bar_,... - -The field under validation must not be present _only if_ any of the other specified fields are present. - - -#### missing_with_all:_foo_,_bar_,... - -The field under validation must not be present _only if_ all of the other specified fields are present. - - -#### not_in:_foo_,_bar_,... - -The field under validation must not be included in the given list of values. The `Rule::notIn` method may be used to fluently construct the rule: - -```php -use Hypervel\Validation\Rule; - -Validator::make($data, [ - 'toppings' => [ - 'required', - Rule::notIn(['sprinkles', 'cherries']), - ], -]); -``` - - -#### not_regex:_pattern_ - -The field under validation must not match the given regular expression. - -Internally, this rule uses the PHP `preg_match` function. The pattern specified should obey the same formatting required by `preg_match` and thus also include valid delimiters. For example: `'email' => 'not_regex:/^.+$/i'`. - -::: warning -When using the `regex` / `not_regex` patterns, it may be necessary to specify your validation rules using an array instead of using `|` delimiters, especially if the regular expression contains a `|` character. -::: - - -#### nullable - -The field under validation may be `null`. - - -#### numeric - -The field under validation must be [numeric](https://www.php.net/manual/en/function.is-numeric.php). - - -#### present - -The field under validation must exist in the input data. - - -#### present_if:_anotherfield_,_value_,... - -The field under validation must be present if the _anotherfield_ field is equal to any _value_. - - -#### present_unless:_anotherfield_,_value_ - -The field under validation must be present unless the _anotherfield_ field is equal to any _value_. - - -#### present_with:_foo_,_bar_,... - -The field under validation must be present _only if_ any of the other specified fields are present. - - -#### present_with_all:_foo_,_bar_,... - -The field under validation must be present _only if_ all of the other specified fields are present. - - -#### prohibited - -The field under validation must be missing or empty. A field is "empty" if it meets one of the following criteria: - -
      - -- The value is `null`. -- The value is an empty string. -- The value is an empty array or empty `Countable` object. -- The value is an uploaded file with an empty path. - -
      - - -#### prohibited_if:_anotherfield_,_value_,... - -The field under validation must be missing or empty if the _anotherfield_ field is equal to any _value_. A field is "empty" if it meets one of the following criteria: - -
      - -- The value is `null`. -- The value is an empty string. -- The value is an empty array or empty `Countable` object. -- The value is an uploaded file with an empty path. - -
      - -If complex conditional prohibition logic is required, you may utilize the `Rule::prohibitedIf` method. This method accepts a boolean or a closure. When given a closure, the closure should return `true` or `false` to indicate if the field under validation should be prohibited: - -```php -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rule; - -Validator::make($request->all(), [ - 'role_id' => Rule::prohibitedIf($request->user()->is_admin), -]); - -Validator::make($request->all(), [ - 'role_id' => Rule::prohibitedIf(fn () => $request->user()->is_admin), -]); -``` - -#### prohibited_if_accepted:_anotherfield_,... - -The field under validation must be missing or empty if the _anotherfield_ field is equal to `"yes"`, `"on"`, `1`, `"1"`, `true`, or `"true"`. - - -#### prohibited_if_declined:_anotherfield_,... - -The field under validation must be missing or empty if the _anotherfield_ field is equal to `"no"`, `"off"`, `0`, `"0"`, `false`, or `"false"`. - - -#### prohibited_unless:_anotherfield_,_value_,... - -The field under validation must be missing or empty unless the _anotherfield_ field is equal to any _value_. A field is "empty" if it meets one of the following criteria: - -
      - -- The value is `null`. -- The value is an empty string. -- The value is an empty array or empty `Countable` object. -- The value is an uploaded file with an empty path. - -
      - - -#### prohibits:_anotherfield_,... - -If the field under validation is not missing or empty, all fields in _anotherfield_ must be missing or empty. A field is "empty" if it meets one of the following criteria: - -
      - -- The value is `null`. -- The value is an empty string. -- The value is an empty array or empty `Countable` object. -- The value is an uploaded file with an empty path. - -
      - - -#### regex:_pattern_ - -The field under validation must match the given regular expression. - -Internally, this rule uses the PHP `preg_match` function. The pattern specified should obey the same formatting required by `preg_match` and thus also include valid delimiters. For example: `'email' => 'regex:/^.+@.+$/i'`. - -::: warning -When using the `regex` / `not_regex` patterns, it may be necessary to specify rules in an array instead of using `|` delimiters, especially if the regular expression contains a `|` character. -::: - - -#### required - -The field under validation must be present in the input data and not empty. A field is "empty" if it meets one of the following criteria: - -
      - -- The value is `null`. -- The value is an empty string. -- The value is an empty array or empty `Countable` object. -- The value is an uploaded file with no path. - -
      - - -#### required_if:_anotherfield_,_value_,... - -The field under validation must be present and not empty if the _anotherfield_ field is equal to any _value_. - -If you would like to construct a more complex condition for the `required_if` rule, you may use the `Rule::requiredIf` method. This method accepts a boolean or a closure. When passed a closure, the closure should return `true` or `false` to indicate if the field under validation is required: - -```php -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rule; - -Validator::make($request->all(), [ - 'role_id' => Rule::requiredIf($request->user()->is_admin), -]); - -Validator::make($request->all(), [ - 'role_id' => Rule::requiredIf(fn () => $request->user()->is_admin), -]); -``` - - -#### required_if_accepted:_anotherfield_,... - -The field under validation must be present and not empty if the _anotherfield_ field is equal to `"yes"`, `"on"`, `1`, `"1"`, `true`, or `"true"`. - - -#### required_if_declined:_anotherfield_,... - -The field under validation must be present and not empty if the _anotherfield_ field is equal to `"no"`, `"off"`, `0`, `"0"`, `false`, or `"false"`. - - -#### required_unless:_anotherfield_,_value_,... - -The field under validation must be present and not empty unless the _anotherfield_ field is equal to any _value_. This also means _anotherfield_ must be present in the request data unless _value_ is `null`. If _value_ is `null` (`required_unless:name,null`), the field under validation will be required unless the comparison field is `null` or the comparison field is missing from the request data. - - -#### required_with:_foo_,_bar_,... - -The field under validation must be present and not empty _only if_ any of the other specified fields are present and not empty. - - -#### required_with_all:_foo_,_bar_,... - -The field under validation must be present and not empty _only if_ all of the other specified fields are present and not empty. - - -#### required_without:_foo_,_bar_,... - -The field under validation must be present and not empty _only when_ any of the other specified fields are empty or not present. - - -#### required_without_all:_foo_,_bar_,... - -The field under validation must be present and not empty _only when_ all of the other specified fields are empty or not present. - - -#### required_array_keys:_foo_,_bar_,... - -The field under validation must be an array and must contain at least the specified keys. - - -#### same:_field_ - -The given _field_ must match the field under validation. - - -#### size:_value_ - -The field under validation must have a size matching the given _value_. For string data, _value_ corresponds to the number of characters. For numeric data, _value_ corresponds to a given integer value (the attribute must also have the `numeric` or `integer` rule). For an array, _size_ corresponds to the `count` of the array. For files, _size_ corresponds to the file size in kilobytes. Let's look at some examples: - -```php -// Validate that a string is exactly 12 characters long... -'title' => 'size:12'; - -// Validate that a provided integer equals 10... -'seats' => 'integer|size:10'; - -// Validate that an array has exactly 5 elements... -'tags' => 'array|size:5'; - -// Validate that an uploaded file is exactly 512 kilobytes... -'image' => 'file|size:512'; -``` - - -#### starts_with:_foo_,_bar_,... - -The field under validation must start with one of the given values. - - -#### string - -The field under validation must be a string. If you would like to allow the field to also be `null`, you should assign the `nullable` rule to the field. - - -#### timezone - -The field under validation must be a valid timezone identifier according to the `DateTimeZone::listIdentifiers` method. - -The arguments [accepted by the `DateTimeZone::listIdentifiers` method](https://www.php.net/manual/en/datetimezone.listidentifiers.php) may also be provided to this validation rule: - -```php -'timezone' => 'required|timezone:all'; - -'timezone' => 'required|timezone:Africa'; - -'timezone' => 'required|timezone:per_country,US'; -``` - - -#### unique:_table_,_column_ - -The field under validation must not exist within the given database table. - -**Specifying a Custom Table / Column Name:** - -Instead of specifying the table name directly, you may specify the Eloquent model which should be used to determine the table name: - -```php -'email' => 'unique:App\Models\User,email_address' -``` - -The `column` option may be used to specify the field's corresponding database column. If the `column` option is not specified, the name of the field under validation will be used. - -```php -'email' => 'unique:users,email_address' -``` - -**Specifying a Custom Database Connection** - -Occasionally, you may need to set a custom connection for database queries made by the Validator. To accomplish this, you may prepend the connection name to the table name: - -```php -'email' => 'unique:connection.users,email_address' -``` - -**Forcing a Unique Rule to Ignore a Given ID:** - -Sometimes, you may wish to ignore a given ID during unique validation. For example, consider an "update profile" screen that includes the user's name, email address, and location. You will probably want to verify that the email address is unique. However, if the user only changes the name field and not the email field, you do not want a validation error to be thrown because the user is already the owner of the email address in question. - -To instruct the validator to ignore the user's ID, we'll use the `Rule` class to fluently define the rule. In this example, we'll also specify the validation rules as an array instead of using the `|` character to delimit the rules: - -```php -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rule; - -Validator::make($data, [ - 'email' => [ - 'required', - Rule::unique('users')->ignore($user->id), - ], -]); -``` - -::: warning -> You should never pass any user controlled request input into the `ignore` method. Instead, you should only pass a system generated unique ID such as an auto-incrementing ID or UUID from an Eloquent model instance. Otherwise, your application will be vulnerable to an SQL injection attack. -::: - -Instead of passing the model key's value to the `ignore` method, you may also pass the entire model instance. Hypervel will automatically extract the key from the model: - -```php -Rule::unique('users')->ignore($user) -``` - -If your table uses a primary key column name other than `id`, you may specify the name of the column when calling the `ignore` method: - -```php -Rule::unique('users')->ignore($user->id, 'user_id') -``` - -By default, the `unique` rule will check the uniqueness of the column matching the name of the attribute being validated. However, you may pass a different column name as the second argument to the `unique` method: - -```php -Rule::unique('users', 'email_address')->ignore($user->id) -``` - -**Adding Additional Where Clauses:** - -You may specify additional query conditions by customizing the query using the `where` method. For example, let's add a query condition that scopes the query to only search records that have an `account_id` column value of `1`: - -```php -'email' => Rule::unique('users')->where(fn (Builder $query) => $query->where('account_id', 1)) -``` - -**Ignoring Soft Deleted Records in Unique Checks:** - -By default, the unique rule includes soft deleted records when determining uniqueness. To exclude soft deleted records from the uniqueness check, you may invoke the `withoutTrashed` method: - -```php -Rule::unique('users')->withoutTrashed(); -``` - -If your model uses a column name other than `deleted_at` for soft deleted records, you may provide the column name when invoking the `withoutTrashed` method: - -```php -Rule::unique('users')->withoutTrashed('was_deleted_at'); -``` - - -#### uppercase - -The field under validation must be uppercase. - - -#### url - -The field under validation must be a valid URL. - -If you would like to specify the URL protocols that should be considered valid, you may pass the protocols as validation rule parameters: - -```php -'url' => 'url:http,https', - -'game' => 'url:minecraft,steam', -``` - - -#### ulid - -The field under validation must be a valid [Universally Unique Lexicographically Sortable Identifier](https://github.com/ulid/spec) (ULID). - - -#### uuid - -The field under validation must be a valid RFC 9562 (version 1, 3, 4, 5, 6, 7, or 8) universally unique identifier (UUID). - -You may also validate that the given UUID matches a UUID specification by version: - -```php -'uuid' => 'uuid:4' -``` - - -## Conditionally Adding Rules - - -#### Skipping Validation When Fields Have Certain Values - -You may occasionally wish to not validate a given field if another field has a given value. You may accomplish this using the `exclude_if` validation rule. In this example, the `appointment_date` and `doctor_name` fields will not be validated if the `has_appointment` field has a value of `false`: - -```php -use Hypervel\Support\Facades\Validator; - -$validator = Validator::make($data, [ - 'has_appointment' => 'required|boolean', - 'appointment_date' => 'exclude_if:has_appointment,false|required|date', - 'doctor_name' => 'exclude_if:has_appointment,false|required|string', -]); -``` - -Alternatively, you may use the `exclude_unless` rule to not validate a given field unless another field has a given value: - -```php -$validator = Validator::make($data, [ - 'has_appointment' => 'required|boolean', - 'appointment_date' => 'exclude_unless:has_appointment,true|required|date', - 'doctor_name' => 'exclude_unless:has_appointment,true|required|string', -]); -``` - -#### Validating When Present - -In some situations, you may wish to run validation checks against a field **only** if that field is present in the data being validated. To quickly accomplish this, add the `sometimes` rule to your rule list: - -```php -$validator = Validator::make($data, [ - 'email' => 'sometimes|required|email', -]); -``` - -In the example above, the `email` field will only be validated if it is present in the `$data` array. - -::: note -If you are attempting to validate a field that should always be present but may be empty, check out [this note on optional fields](#a-note-on-optional-fields). -::: - -#### Complex Conditional Validation - -Sometimes you may wish to add validation rules based on more complex conditional logic. For example, you may wish to require a given field only if another field has a greater value than 100. Or, you may need two fields to have a given value only when another field is present. Adding these validation rules doesn't have to be a pain. First, create a `Validator` instance with your _static rules_ that never change: - -```php -use Hypervel\Support\Facades\Validator; - -$validator = Validator::make($request->all(), [ - 'email' => 'required|email', - 'games' => 'required|integer|min:0', -]); -``` - -Let's assume our web application is for game collectors. If a game collector registers with our application and they own more than 100 games, we want them to explain why they own so many games. For example, perhaps they run a game resale shop, or maybe they just enjoy collecting games. To conditionally add this requirement, we can use the `sometimes` method on the `Validator` instance. - -```php -use Hypervel\Support\Fluent; - -$validator->sometimes('reason', 'required|max:500', function (Fluent $input) { - return $input->games >= 100; -}); -``` - -The first argument passed to the `sometimes` method is the name of the field we are conditionally validating. The second argument is a list of the rules we want to add. If the closure passed as the third argument returns `true`, the rules will be added. This method makes it a breeze to build complex conditional validations. You may even add conditional validations for several fields at once: - -```php -$validator->sometimes(['reason', 'cost'], 'required', function (Fluent $input) { - return $input->games >= 100; -}); -``` - -::: note -The `$input` parameter passed to your closure will be an instance of `Hypervel\Support\Fluent` and may be used to access your input and files under validation. -::: - -#### Complex Conditional Array Validation - -Sometimes you may want to validate a field based on another field in the same nested array whose index you do not know. In these situations, you may allow your closure to receive a second argument which will be the current individual item in the array being validated: - -```php -$input = [ - 'channels' => [ - [ - 'type' => 'email', - 'address' => 'abigail@example.com', - ], - [ - 'type' => 'url', - 'address' => 'https://example.com', - ], - ], -]; - -$validator->sometimes('channels.*.address', 'email', function (Fluent $input, Fluent $item) { - return $item->type === 'email'; -}); - -$validator->sometimes('channels.*.address', 'url', function (Fluent $input, Fluent $item) { - return $item->type !== 'email'; -}); -``` - -Like the `$input` parameter passed to the closure, the `$item` parameter is an instance of `Hypervel\Support\Fluent` when the attribute data is an array; otherwise, it is a string. - -## Validating Arrays - -As discussed in the [array validation rule documentation](#rule-array), the `array` rule accepts a list of allowed array keys. If any additional keys are present within the array, validation will fail: - -```php -use Hypervel\Support\Facades\Validator; - -$input = [ - 'user' => [ - 'name' => 'Taylor Otwell', - 'username' => 'taylorotwell', - 'admin' => true, - ], -]; - -Validator::make($input, [ - 'user' => 'array:name,username', -]); -``` - -In general, you should always specify the array keys that are allowed to be present within your array. Otherwise, the validator's `validate` and `validated` methods will return all of the validated data, including the array and all of its keys, even if those keys were not validated by other nested array validation rules. - -### Validating Nested Array Input - -Validating nested array based form input fields doesn't have to be a pain. You may use "dot notation" to validate attributes within an array. For example, if the incoming HTTP request contains a `photos[profile]` field, you may validate it like so: - -```php -use Hypervel\Support\Facades\Validator; - -$validator = Validator::make($request->all(), [ - 'photos.profile' => 'required|image', -]); -``` - -You may also validate each element of an array. For example, to validate that each email in a given array input field is unique, you may do the following: - -```php -$validator = Validator::make($request->all(), [ - 'person.*.email' => 'email|unique:users', - 'person.*.first_name' => 'required_with:person.*.last_name', -]); -``` - -Likewise, you may use the `*` character when specifying [custom validation messages in your language files](#custom-messages-for-specific-attributes), making it a breeze to use a single validation message for array based fields: - -```php -'custom' => [ - 'person.*.email' => [ - 'unique' => 'Each person must have a unique email address', - ] -], -``` - -#### Accessing Nested Array Data - -Sometimes you may need to access the value for a given nested array element when assigning validation rules to the attribute. You may accomplish this using the `Rule::forEach` method. The `forEach` method accepts a closure that will be invoked for each iteration of the array attribute under validation and will receive the attribute's value and explicit, fully-expanded attribute name. The closure should return an array of rules to assign to the array element: - -```php -use App\Rules\HasPermission; -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rule; - -$validator = Validator::make($request->all(), [ - 'companies.*.id' => Rule::forEach(function (string|null $value, string $attribute) { - return [ - Rule::exists(Company::class, 'id'), - new HasPermission('manage-company', $value), - ]; - }), -]); -``` - -### Error Message Indexes and Positions - -When validating arrays, you may want to reference the index or position of a particular item that failed validation within the error message displayed by your application. To accomplish this, you may include the `:index` (starts from `0`) and `:position` (starts from `1`) placeholders within your [custom validation message](#manual-customizing-the-error-messages): - -```php -use Hypervel\Support\Facades\Validator; - -$input = [ - 'photos' => [ - [ - 'name' => 'BeachVacation.jpg', - 'description' => 'A photo of my beach vacation!', - ], - [ - 'name' => 'GrandCanyon.jpg', - 'description' => '', - ], - ], -]; - -Validator::validate($input, [ - 'photos.*.description' => 'required', -], [ - 'photos.*.description.required' => 'Please describe photo #:position.', -]); -``` - -Given the example above, validation will fail and the user will be presented with the following error of _"Please describe photo #2."_ - -If necessary, you may reference more deeply nested indexes and positions via `second-index`, `second-position`, `third-index`, `third-position`, etc. - -```php -'photos.*.attributes.*.string' => 'Invalid attribute for photo #:second-position.', -``` - -## Validating Files - -Hypervel provides a variety of validation rules that may be used to validate uploaded files, such as `mimes`, `image`, `min`, and `max`. While you are free to specify these rules individually when validating files, Hypervel also offers a fluent file validation rule builder that you may find convenient: - -```php -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rules\File; - -Validator::validate($input, [ - 'attachment' => [ - 'required', - File::types(['mp3', 'wav']) - ->min(1024) - ->max(12 * 1024), - ], -]); -``` - -#### Validating File Types - -Even though you only need to specify the extensions when invoking the `types` method, this method actually validates the MIME type of the file by reading the file's contents and guessing its MIME type. A full listing of MIME types and their corresponding extensions may be found at the following location: - -[https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types](https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types) - - -#### Validating File Sizes - -For convenience, minimum and maximum file sizes may be specified as a string with a suffix indicating the file size units. The `kb`, `mb`, `gb`, and `tb` suffixes are supported: - -```php -File::types(['mp3', 'wav']) - ->min('1kb') - ->max('10mb'); -``` - - -#### Validating Image Files - -If your application accepts images uploaded by your users, you may use the `File` rule's `image` constructor method to ensure that the file under validation is an image (jpg, jpeg, png, bmp, gif, or webp). - -In addition, the `dimensions` rule may be used to limit the dimensions of the image: - -```php -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rule; -use Hypervel\Validation\Rules\File; - -Validator::validate($input, [ - 'photo' => [ - 'required', - File::image() - ->min(1024) - ->max(12 * 1024) - ->dimensions(Rule::dimensions()->maxWidth(1000)->maxHeight(500)), - ], -]); -``` - -::: note -More information regarding validating image dimensions may be found in the [dimension rule documentation](#rule-dimensions). -::: - -::: warning -By default, the `image` rule does not allow SVG files due to the possibility of XSS vulnerabilities. If you need to allow SVG files, you may pass `allowSvg: true` to the `image` rule: `File::image(allowSvg: true)`. -::: - - -#### Validating Image Dimensions - -You may also validate the dimensions of an image. For example, to validate that an uploaded image is at least 1000 pixels wide and 500 pixels tall, you may use the `dimensions` rule: - -```php -use Hypervel\Validation\Rule; -use Hypervel\Validation\Rules\File; - -File::image()->dimensions( - Rule::dimensions() - ->maxWidth(1000) - ->maxHeight(500) -) -``` - -::: note -More information regarding validating image dimensions may be found in the [dimension rule documentation](#rule-dimensions). -::: - -## Validating Passwords - -To ensure that passwords have an adequate level of complexity, you may use Hypervel's `Password` rule object: - -```php -use Hypervel\Support\Facades\Validator; -use Hypervel\Validation\Rules\Password; - -$validator = Validator::make($request->all(), [ - 'password' => ['required', 'confirmed', Password::min(8)], -]); -``` - -The `Password` rule object allows you to easily customize the password complexity requirements for your application, such as specifying that passwords require at least one letter, number, symbol, or characters with mixed casing: - -```php -// Require at least 8 characters... -Password::min(8) - -// Require at least one letter... -Password::min(8)->letters() - -// Require at least one uppercase and one lowercase letter... -Password::min(8)->mixedCase() - -// Require at least one number... -Password::min(8)->numbers() - -// Require at least one symbol... -Password::min(8)->symbols() -``` - -In addition, you may ensure that a password has not been compromised in a public password data breach leak using the `uncompromised` method: - -```php -Password::min(8)->uncompromised() -``` - -Internally, the `Password` rule object uses the [k-Anonymity](https://en.wikipedia.org/wiki/K-anonymity) model to determine if a password has been leaked via the [haveibeenpwned.com](https://haveibeenpwned.com) service without sacrificing the user's privacy or security. - -By default, if a password appears at least once in a data leak, it will be considered compromised. You can customize this threshold using the first argument of the `uncompromised` method: - -```php -// Ensure the password appears less than 3 times in the same data leak... -Password::min(8)->uncompromised(3); -``` - -Of course, you may chain all the methods in the examples above: - -```php -Password::min(8) - ->letters() - ->mixedCase() - ->numbers() - ->symbols() - ->uncompromised() -``` - -#### Defining Default Password Rules - -You may find it convenient to specify the default validation rules for passwords in a single location of your application. You can easily accomplish this using the `Password::defaults` method, which accepts a closure. The closure given to the `defaults` method should return the default configuration of the Password rule. Typically, the `defaults` rule should be called within the `boot` method of one of your application's service providers: - -```php -use Hypervel\Validation\Rules\Password; - -/** - * Bootstrap any application services. - */ -public function boot(): void -{ - Password::defaults(function () { - $rule = Password::min(8); - - return $this->app->isProduction() - ? $rule->mixedCase()->uncompromised() - : $rule; - }); -} -``` - -Then, when you would like to apply the default rules to a particular password undergoing validation, you may invoke the `defaults` method with no arguments: - -```php -'password' => ['required', Password::defaults()], -``` - -Occasionally, you may want to attach additional validation rules to your default password validation rules. You may use the `rules` method to accomplish this: - -```php -use App\Rules\ZxcvbnRule; - -Password::defaults(function () { - $rule = Password::min(8)->rules([new ZxcvbnRule]); - - // ... -}); -``` - -## Custom Validation Rules - -### Using Rule Objects - -Hypervel provides a variety of helpful validation rules; however, you may wish to specify some of your own. One method of registering custom validation rules is using rule objects. To generate a new rule object, you may use the `make:rule` Artisan command. Let's use this command to generate a rule that verifies a string is uppercase. Hypervel will place the new rule in the `app/Rules` directory. If this directory does not exist, Hypervel will create it when you execute the Artisan command to create your rule: - -```shell -php artisan make:rule Uppercase -``` - -Once the rule has been created, we are ready to define its behavior. A rule object contains a single method: `validate`. This method receives the attribute name, its value, and a callback that should be invoked on failure with the validation error message: - -```php -validate([ - 'name' => ['required', 'string', new Uppercase], -]); -``` - -#### Translating Validation Messages - -Instead of providing a literal error message to the `$fail` closure, you may also provide a [translation string key](/docs/localization) and instruct Hypervel to translate the error message: - -```php -if (strtoupper($value) !== $value) { - $fail('validation.uppercase')->translate(); -} -``` - -If necessary, you may provide placeholder replacements and the preferred language as the first and second arguments to the `translate` method: - -```php -$fail('validation.location')->translate([ - 'value' => $this->value, -], 'fr'); -``` - -#### Accessing Additional Data - -If your custom validation rule class needs to access all of the other data undergoing validation, your rule class may implement the `Hypervel\Validation\Contracts\DataAwareRule` interface. This interface requires your class to define a `setData` method. This method will automatically be invoked by Hypervel (before validation proceeds) with all of the data under validation: - -```php - - */ - protected array $data = []; - - // ... - - /** - * Set the data under validation. - * - * @param array $data - */ - public function setData(array $data): static - { - $this->data = $data; - - return $this; - } -} -``` - -Or, if your validation rule requires access to the validator instance performing the validation, you may implement the `ValidatorAwareRule` interface: - -```php -validator = $validator; - - return $this; - } -} -``` - -### Using Closures - -If you only need the functionality of a custom rule once throughout your application, you may use a closure instead of a rule object. The closure receives the attribute's name, the attribute's value, and a `$fail` callback that should be called if validation fails: - -```php -use Hypervel\Support\Facades\Validator; -use Closure; - -$validator = Validator::make($request->all(), [ - 'title' => [ - 'required', - 'max:255', - function (string $attribute, mixed $value, Closure $fail) { - if ($value === 'foo') { - $fail("The {$attribute} is invalid."); - } - }, - ], -]); -``` - -### Implicit Rules - -By default, when an attribute being validated is not present or contains an empty string, normal validation rules, including custom rules, are not run. For example, the [unique](#rule-unique) rule will not be run against an empty string: - -```php -use Hypervel\Support\Facades\Validator; - -$rules = ['name' => 'unique:users,name']; - -$input = ['name' => '']; - -Validator::make($input, $rules)->passes(); // true -``` - -For a custom rule to run even when an attribute is empty, the rule must imply that the attribute is required. - -::: warning -An "implicit" rule only _implies_ that the attribute is required. Whether it actually invalidates a missing or empty attribute is up to you. -::: \ No newline at end of file diff --git a/_archive/docs/0.3/docs/views.md b/_archive/docs/0.3/docs/views.md deleted file mode 100644 index a6b80447d1..0000000000 --- a/_archive/docs/0.3/docs/views.md +++ /dev/null @@ -1,273 +0,0 @@ -# Views -[[toc]] - -## Introduction - -Of course, it's not practical to return entire HTML documents strings directly from your routes and controllers. Thankfully, views provide a convenient way to place all of our HTML in separate files. - -Views separate your controller / application logic from your presentation logic and are stored in the `resources/views` directory. - -When using Hypervel, view templates are usually written using the [Blade templating language](/docs/blade). A simple view might look something like this: - -```html - - - - -

      Hello, {{ $name }}

      - - -``` - -Since this view is stored at `resources/views/greeting.blade.php`, we may return it using the global `view` helper like so: - -```php -Route::get('/', function () { - return view('greeting', ['name' => 'James']); -}); -``` - -::: note -Looking for more information on how to write Blade templates? Check out the full [Blade documentation](/docs/blade) to get started. -::: - - -## Creating and Rendering Views - -You may create a view by placing a file with the `.blade.php` extension in your application's `resources/views` directory. - -The `.blade.php` extension informs the framework that the file contains a [Blade template](/docs/blade). Blade templates contain HTML as well as Blade directives that allow you to easily echo values, create "if" statements, iterate over data, and more. - -Once you have created a view, you may return it from one of your application's routes or controllers using the global `view` helper: - -```php -Route::get('/', function () { - return view('greeting', ['name' => 'James']); -}); -``` - -Views may also be returned using the `View` facade: - -```php -use Hypervel\Support\Facades\View; - -return View::make('greeting', ['name' => 'James']); -``` - -As you can see, the first argument passed to the `view` helper corresponds to the name of the view file in the `resources/views` directory. The second argument is an array of data that should be made available to the view. In this case, we are passing the `name` variable, which is displayed in the view using [Blade syntax](/docs/blade). - -### Nested View Directories - -Views may also be nested within subdirectories of the `resources/views` directory. "Dot" notation may be used to reference nested views. For example, if your view is stored at `resources/views/admin/profile.blade.php`, you may return it from one of your application's routes / controllers like so: - -```php -return view('admin.profile', $data); -``` - -::: warning -View directory names should not contain the `.` character. -::: - -### Creating the First Available View - -Using the `View` facade's `first` method, you may create the first view that exists in a given array of views. This may be useful if your application or package allows views to be customized or overwritten: - -```php -use Hypervel\Support\Facades\View; - -return View::first(['custom.admin', 'admin'], $data); -``` - - -### Determining if a View Exists - -If you need to determine if a view exists, you may use the `View` facade. The `exists` method will return `true` if the view exists: - -```php -use Hypervel\Support\Facades\View; - -if (View::exists('admin.profile')) { - // ... -} -``` - -## Passing Data to Views - -As you saw in the previous examples, you may pass an array of data to views to make that data available to the view: - -```php -return view('greetings', ['name' => 'Victoria']); -``` - -When passing information in this manner, the data should be an array with key / value pairs. After providing data to a view, you can then access each value within your view using the data's keys, such as ``. - -As an alternative to passing a complete array of data to the `view` helper function, you may use the `with` method to add individual pieces of data to the view. The `with` method returns an instance of the view object so that you can continue chaining methods before returning the view: - -```php -return view('greeting') - ->with('name', 'Victoria') - ->with('occupation', 'Astronaut'); -``` - -### Sharing Data With All Views - -Occasionally, you may need to share data with all views that are rendered by your application. You may do so using the `View` facade's `share` method. Typically, you should place calls to the `share` method within a service provider's `boot` method. You are free to add them to the `App\Providers\AppServiceProvider` class or generate a separate service provider to house them: - -```php -with('count', $this->users->count()); - } -} -``` - -As you can see, all view composers are resolved via the [service container](/docs/container), so you may type-hint any dependencies you need within a composer's constructor. - -#### Attaching a Composer to Multiple Views - -You may attach a view composer to multiple views at once by passing an array of views as the first argument to the `composer` method: - -```php -use App\Views\Composers\MultiComposer; -use Hypervel\Support\Facades\View; - -View::composer( - ['profile', 'dashboard'], - MultiComposer::class -); -``` - -The `composer` method also accepts the `*` character as a wildcard, allowing you to attach a composer to all views: - -```php -use Hypervel\SupportFacades; -use Hypervel\Support\Facades\View; - -Facades\View::composer('*', function (View $view) { - // ... -}); -``` - -### View Creators - -View "creators" are very similar to view composers; however, they are executed immediately after the view is instantiated instead of waiting until the view is about to render. To register a view creator, use the `creator` method: - -```php -use App\View\Creators\ProfileCreator; -use Hypervel\Support\Facades\View; - -View::creator('profile', ProfileCreator::class); -``` - -## View Caching - -By default, Blade template views are compiled on demand. When a request is executed that renders a view, Hypervel will determine if a compiled version of the view exists. If the file exists, Hypervel will then determine if the uncompiled view has been modified more recently than the compiled view. If the compiled view either does not exist, or the uncompiled view has been modified, Hypervel will recompile the view. - -The cache is stored in the `storage/framework/views` directory by default. You can change the cache path in the `config/view.php` file. - -```php -'config' => [ - //... - 'cache_path' => storage_path('framework/views'), -], -``` \ No newline at end of file diff --git a/_archive/docs/0.3/package.json b/_archive/docs/0.3/package.json deleted file mode 100644 index 969f4355b9..0000000000 --- a/_archive/docs/0.3/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "hypervel-docs", - "version": "0.0.1", - "description": "A VuePress project", - "license": "MIT", - "type": "module", - "scripts": { - "docs:build": "vuepress build", - "docs:clean-dev": "vuepress dev --clean-cache", - "docs:dev": "vuepress dev", - "docs:update-package": "pnpm dlx vp-update" - }, - "devDependencies": { - "@vuepress/bundler-vite": "^2.0.0-rc.7", - "@vuepress/plugin-docsearch": "2.0.0-rc.42", - "@vuepress/plugin-redirect": "2.0.0-rc.42", - "@vuepress/plugin-seo": "2.0.0-rc.46", - "@vuepress/theme-default": "^2.0.0-rc.11", - "chart.js": "^4.4.4", - "vue": "^3.4.0", - "vuepress": "^2.0.0-rc.7", - "vuepress-plugin-md-enhance": "2.0.0-rc.52", - "vuepress-plugin-remove-html-extension": "^1.13.0" - } -} diff --git a/_archive/docs/0.3/pnpm-lock.yaml b/_archive/docs/0.3/pnpm-lock.yaml deleted file mode 100644 index 9463afe93b..0000000000 --- a/_archive/docs/0.3/pnpm-lock.yaml +++ /dev/null @@ -1,3492 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -devDependencies: - '@vuepress/bundler-vite': - specifier: ^2.0.0-rc.7 - version: 2.0.0-rc.7 - '@vuepress/plugin-docsearch': - specifier: 2.0.0-rc.42 - version: 2.0.0-rc.42(@algolia/client-search@5.4.0)(search-insights@2.17.2)(vuepress@2.0.0-rc.15) - '@vuepress/plugin-redirect': - specifier: 2.0.0-rc.42 - version: 2.0.0-rc.42(vuepress@2.0.0-rc.15) - '@vuepress/plugin-seo': - specifier: 2.0.0-rc.46 - version: 2.0.0-rc.46(vuepress@2.0.0-rc.15) - '@vuepress/theme-default': - specifier: ^2.0.0-rc.11 - version: 2.0.0-rc.41(vuepress@2.0.0-rc.15) - chart.js: - specifier: ^4.4.4 - version: 4.4.4 - vue: - specifier: ^3.4.0 - version: 3.4.38 - vuepress: - specifier: ^2.0.0-rc.7 - version: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - vuepress-plugin-md-enhance: - specifier: 2.0.0-rc.52 - version: 2.0.0-rc.52(chart.js@4.4.4)(markdown-it@14.1.0)(vuepress@2.0.0-rc.15) - vuepress-plugin-remove-html-extension: - specifier: ^1.13.0 - version: 1.13.0 - -packages: - - /@algolia/autocomplete-core@1.9.3(@algolia/client-search@5.4.0)(algoliasearch@4.24.0)(search-insights@2.17.2): - resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==} - dependencies: - '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@5.4.0)(algoliasearch@4.24.0)(search-insights@2.17.2) - '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@5.4.0)(algoliasearch@4.24.0) - transitivePeerDependencies: - - '@algolia/client-search' - - algoliasearch - - search-insights - dev: true - - /@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@5.4.0)(algoliasearch@4.24.0)(search-insights@2.17.2): - resolution: {integrity: sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==} - peerDependencies: - search-insights: '>= 1 < 3' - dependencies: - '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@5.4.0)(algoliasearch@4.24.0) - search-insights: 2.17.2 - transitivePeerDependencies: - - '@algolia/client-search' - - algoliasearch - dev: true - - /@algolia/autocomplete-preset-algolia@1.9.3(@algolia/client-search@5.4.0)(algoliasearch@4.24.0): - resolution: {integrity: sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==} - peerDependencies: - '@algolia/client-search': '>= 4.9.1 < 6' - algoliasearch: '>= 4.9.1 < 6' - dependencies: - '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@5.4.0)(algoliasearch@4.24.0) - '@algolia/client-search': 5.4.0 - algoliasearch: 4.24.0 - dev: true - - /@algolia/autocomplete-shared@1.9.3(@algolia/client-search@5.4.0)(algoliasearch@4.24.0): - resolution: {integrity: sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==} - peerDependencies: - '@algolia/client-search': '>= 4.9.1 < 6' - algoliasearch: '>= 4.9.1 < 6' - dependencies: - '@algolia/client-search': 5.4.0 - algoliasearch: 4.24.0 - dev: true - - /@algolia/cache-browser-local-storage@4.24.0: - resolution: {integrity: sha512-t63W9BnoXVrGy9iYHBgObNXqYXM3tYXCjDSHeNwnsc324r4o5UiVKUiAB4THQ5z9U5hTj6qUvwg/Ez43ZD85ww==} - dependencies: - '@algolia/cache-common': 4.24.0 - dev: true - - /@algolia/cache-common@4.24.0: - resolution: {integrity: sha512-emi+v+DmVLpMGhp0V9q9h5CdkURsNmFC+cOS6uK9ndeJm9J4TiqSvPYVu+THUP8P/S08rxf5x2P+p3CfID0Y4g==} - dev: true - - /@algolia/cache-in-memory@4.24.0: - resolution: {integrity: sha512-gDrt2so19jW26jY3/MkFg5mEypFIPbPoXsQGQWAi6TrCPsNOSEYepBMPlucqWigsmEy/prp5ug2jy/N3PVG/8w==} - dependencies: - '@algolia/cache-common': 4.24.0 - dev: true - - /@algolia/client-account@4.24.0: - resolution: {integrity: sha512-adcvyJ3KjPZFDybxlqnf+5KgxJtBjwTPTeyG2aOyoJvx0Y8dUQAEOEVOJ/GBxX0WWNbmaSrhDURMhc+QeevDsA==} - dependencies: - '@algolia/client-common': 4.24.0 - '@algolia/client-search': 4.24.0 - '@algolia/transporter': 4.24.0 - dev: true - - /@algolia/client-analytics@4.24.0: - resolution: {integrity: sha512-y8jOZt1OjwWU4N2qr8G4AxXAzaa8DBvyHTWlHzX/7Me1LX8OayfgHexqrsL4vSBcoMmVw2XnVW9MhL+Y2ZDJXg==} - dependencies: - '@algolia/client-common': 4.24.0 - '@algolia/client-search': 4.24.0 - '@algolia/requester-common': 4.24.0 - '@algolia/transporter': 4.24.0 - dev: true - - /@algolia/client-common@4.24.0: - resolution: {integrity: sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==} - dependencies: - '@algolia/requester-common': 4.24.0 - '@algolia/transporter': 4.24.0 - dev: true - - /@algolia/client-common@5.4.0: - resolution: {integrity: sha512-nt6S04aitlsvmah4XNSNaH2KPlbK/YMI1hlXLdxHNvOyK8aQ4IVDaffpRbRzGCdgCc3d7BgUuEcUUZmvaV1Img==} - engines: {node: '>= 14.0.0'} - dev: true - - /@algolia/client-personalization@4.24.0: - resolution: {integrity: sha512-l5FRFm/yngztweU0HdUzz1rC4yoWCFo3IF+dVIVTfEPg906eZg5BOd1k0K6rZx5JzyyoP4LdmOikfkfGsKVE9w==} - dependencies: - '@algolia/client-common': 4.24.0 - '@algolia/requester-common': 4.24.0 - '@algolia/transporter': 4.24.0 - dev: true - - /@algolia/client-search@4.24.0: - resolution: {integrity: sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==} - dependencies: - '@algolia/client-common': 4.24.0 - '@algolia/requester-common': 4.24.0 - '@algolia/transporter': 4.24.0 - dev: true - - /@algolia/client-search@5.4.0: - resolution: {integrity: sha512-LOLNfYljPdHNhVqb36hdkHwti8Cjjnkn4FCuhgqWfhUVkRslgUD8jD8+NaY6O3s9CLnIrslnZseXNhWk+Fwmgw==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.4.0 - '@algolia/requester-browser-xhr': 5.4.0 - '@algolia/requester-fetch': 5.4.0 - '@algolia/requester-node-http': 5.4.0 - dev: true - - /@algolia/logger-common@4.24.0: - resolution: {integrity: sha512-LLUNjkahj9KtKYrQhFKCzMx0BY3RnNP4FEtO+sBybCjJ73E8jNdaKJ/Dd8A/VA4imVHP5tADZ8pn5B8Ga/wTMA==} - dev: true - - /@algolia/logger-console@4.24.0: - resolution: {integrity: sha512-X4C8IoHgHfiUROfoRCV+lzSy+LHMgkoEEU1BbKcsfnV0i0S20zyy0NLww9dwVHUWNfPPxdMU+/wKmLGYf96yTg==} - dependencies: - '@algolia/logger-common': 4.24.0 - dev: true - - /@algolia/recommend@4.24.0: - resolution: {integrity: sha512-P9kcgerfVBpfYHDfVZDvvdJv0lEoCvzNlOy2nykyt5bK8TyieYyiD0lguIJdRZZYGre03WIAFf14pgE+V+IBlw==} - dependencies: - '@algolia/cache-browser-local-storage': 4.24.0 - '@algolia/cache-common': 4.24.0 - '@algolia/cache-in-memory': 4.24.0 - '@algolia/client-common': 4.24.0 - '@algolia/client-search': 4.24.0 - '@algolia/logger-common': 4.24.0 - '@algolia/logger-console': 4.24.0 - '@algolia/requester-browser-xhr': 4.24.0 - '@algolia/requester-common': 4.24.0 - '@algolia/requester-node-http': 4.24.0 - '@algolia/transporter': 4.24.0 - dev: true - - /@algolia/requester-browser-xhr@4.24.0: - resolution: {integrity: sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==} - dependencies: - '@algolia/requester-common': 4.24.0 - dev: true - - /@algolia/requester-browser-xhr@5.4.0: - resolution: {integrity: sha512-mNZil+Q95oGL2IhLQmJHF0vMZV/Ku8RlO0/dwbCRKZcHmVGzc2DC1EK2+5vRNJtjWsZFwlZ/pC7HhSg0xAnQlA==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.4.0 - dev: true - - /@algolia/requester-common@4.24.0: - resolution: {integrity: sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA==} - dev: true - - /@algolia/requester-fetch@5.4.0: - resolution: {integrity: sha512-xF7dzYT+KybaJGd0DETqJ164LPebqe64zjYMEfnjcncJ/nJrjUG3e+XPaWZz0vcEnT+KHeqC8FrbLym3AG/SGQ==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.4.0 - dev: true - - /@algolia/requester-node-http@4.24.0: - resolution: {integrity: sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==} - dependencies: - '@algolia/requester-common': 4.24.0 - dev: true - - /@algolia/requester-node-http@5.4.0: - resolution: {integrity: sha512-PckoNE0vx89yjsJSzn/lAHI0eSWheBXbXS0NaJ09DWyfRuJCzTZ0lPxZSPlo7jwR6Lyjtzl27gSwSPUVznoHRw==} - engines: {node: '>= 14.0.0'} - dependencies: - '@algolia/client-common': 5.4.0 - dev: true - - /@algolia/transporter@4.24.0: - resolution: {integrity: sha512-86nI7w6NzWxd1Zp9q3413dRshDqAzSbsQjhcDhPIatEFiZrL1/TjnHL8S7jVKFePlIMzDsZWXAXwXzcok9c5oA==} - dependencies: - '@algolia/cache-common': 4.24.0 - '@algolia/logger-common': 4.24.0 - '@algolia/requester-common': 4.24.0 - dev: true - - /@babel/helper-string-parser@7.24.8: - resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-validator-identifier@7.24.7: - resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/parser@7.25.3: - resolution: {integrity: sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==} - engines: {node: '>=6.0.0'} - hasBin: true - dependencies: - '@babel/types': 7.25.2 - dev: true - - /@babel/types@7.25.2: - resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.24.8 - '@babel/helper-validator-identifier': 7.24.7 - to-fast-properties: 2.0.0 - dev: true - - /@docsearch/css@3.6.1: - resolution: {integrity: sha512-VtVb5DS+0hRIprU2CO6ZQjK2Zg4QU5HrDM1+ix6rT0umsYvFvatMAnf97NHZlVWDaaLlx7GRfR/7FikANiM2Fg==} - dev: true - - /@docsearch/js@3.6.1(@algolia/client-search@5.4.0)(search-insights@2.17.2): - resolution: {integrity: sha512-erI3RRZurDr1xES5hvYJ3Imp7jtrXj6f1xYIzDzxiS7nNBufYWPbJwrmMqWC5g9y165PmxEmN9pklGCdLi0Iqg==} - dependencies: - '@docsearch/react': 3.6.1(@algolia/client-search@5.4.0)(search-insights@2.17.2) - preact: 10.23.2 - transitivePeerDependencies: - - '@algolia/client-search' - - '@types/react' - - react - - react-dom - - search-insights - dev: true - - /@docsearch/react@3.6.1(@algolia/client-search@5.4.0)(search-insights@2.17.2): - resolution: {integrity: sha512-qXZkEPvybVhSXj0K7U3bXc233tk5e8PfhoZ6MhPOiik/qUQxYC+Dn9DnoS7CxHQQhHfCvTiN0eY9M12oRghEXw==} - peerDependencies: - '@types/react': '>= 16.8.0 < 19.0.0' - react: '>= 16.8.0 < 19.0.0' - react-dom: '>= 16.8.0 < 19.0.0' - search-insights: '>= 1 < 3' - peerDependenciesMeta: - '@types/react': - optional: true - react: - optional: true - react-dom: - optional: true - search-insights: - optional: true - dependencies: - '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@5.4.0)(algoliasearch@4.24.0)(search-insights@2.17.2) - '@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@5.4.0)(algoliasearch@4.24.0) - '@docsearch/css': 3.6.1 - algoliasearch: 4.24.0 - search-insights: 2.17.2 - transitivePeerDependencies: - - '@algolia/client-search' - dev: true - - /@esbuild/aix-ppc64@0.19.12: - resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true - optional: true - - /@esbuild/aix-ppc64@0.21.5: - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm64@0.19.12: - resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm64@0.21.5: - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.19.12: - resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.21.5: - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.19.12: - resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.21.5: - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.19.12: - resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.21.5: - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.19.12: - resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.21.5: - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.19.12: - resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.21.5: - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.19.12: - resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.21.5: - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.19.12: - resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.21.5: - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.19.12: - resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.21.5: - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.19.12: - resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.21.5: - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.19.12: - resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.21.5: - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.19.12: - resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.21.5: - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.19.12: - resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.21.5: - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.19.12: - resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.21.5: - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.19.12: - resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.21.5: - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.19.12: - resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.21.5: - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.19.12: - resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.21.5: - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.19.12: - resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.21.5: - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.19.12: - resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.21.5: - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.19.12: - resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.21.5: - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.19.12: - resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.21.5: - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.19.12: - resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.21.5: - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@jridgewell/sourcemap-codec@1.5.0: - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - dev: true - - /@kurkle/color@0.3.2: - resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} - dev: true - - /@mdit-vue/plugin-component@2.1.3: - resolution: {integrity: sha512-9AG17beCgpEw/4ldo/M6Y/1Rh4E1bqMmr/rCkWKmCAxy9tJz3lzY7HQJanyHMJufwsb3WL5Lp7Om/aPcQTZ9SA==} - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit-vue/plugin-frontmatter@2.1.3: - resolution: {integrity: sha512-KxsSCUVBEmn6sJcchSTiI5v9bWaoRxe68RBYRDGcSEY1GTnfQ5gQPMIsM48P4q1luLEIWurVGGrRu7u93//LDQ==} - dependencies: - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - gray-matter: 4.0.3 - markdown-it: 14.1.0 - dev: true - - /@mdit-vue/plugin-headers@2.1.3: - resolution: {integrity: sha512-AcL7a7LHQR3ISINhfjGJNE/bHyM0dcl6MYm1Sr//zF7ZgokPGwD/HhD7TzwmrKA9YNYCcO9P3QmF/RN9XyA6CA==} - dependencies: - '@mdit-vue/shared': 2.1.3 - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit-vue/plugin-sfc@2.1.3: - resolution: {integrity: sha512-Ezl0dNvQNS639Yl4siXm+cnWtQvlqHrg+u+lnau/OHpj9Xh3LVap/BSQVugKIV37eR13jXXYf3VaAOP1fXPN+w==} - dependencies: - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit-vue/plugin-title@2.1.3: - resolution: {integrity: sha512-XWVOQoZqczoN97xCDrnQicmXKoqwOjIymIm9HQnRXhHnYKOgJPW1CxSGhkcOGzvDU1v0mD/adojVyyj/s6ggWw==} - dependencies: - '@mdit-vue/shared': 2.1.3 - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit-vue/plugin-toc@2.1.3: - resolution: {integrity: sha512-41Q+iXpLHZt0zJdApVwoVt7WF6za/xUjtjEPf90Z3KLzQO01TXsv48Xp9BsrFHPcPcm8tiZ0+O1/ICJO80V/MQ==} - dependencies: - '@mdit-vue/shared': 2.1.3 - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit-vue/shared@2.1.3: - resolution: {integrity: sha512-27YI8b0VVZsAlNwaWoaOCWbr4eL8B04HxiYk/y2ktblO/nMcOEOLt4p0RjuobvdyUyjHvGOS09RKhq7qHm1CHQ==} - dependencies: - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit-vue/types@2.1.0: - resolution: {integrity: sha512-TMBB/BQWVvwtpBdWD75rkZx4ZphQ6MN0O4QB2Bc0oI5PC2uE57QerhNxdRZ7cvBHE2iY2C+BUNUziCfJbjIRRA==} - dev: true - - /@mdit/plugin-alert@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-4OyGK1PZrJbmEF/kS6GKmmG1nlN5h/CyIPZV8lRgnlWLFB37JiEz3EHusPAXAoMtw7VGNFaIcl7OT/I5yyz1JQ==} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-align@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-rvA+xzaVrlsr44s7XD/xadO3lF0QYWCbeSrOS2dhOroNCIOy4RotVP/1tQPr84eqm4oXcxXF0cbjFuwUgE1jYw==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@mdit/plugin-container': 0.12.0(markdown-it@14.1.0) - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-attrs@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-J0MBwBq958lBtdIcEo02mUIO4ubl2YK+bY799T2SusrLTf3FZsq8+d/OiLTUtovfxaphD7F6yqo8M61AiOpq+w==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-container@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-61bWK1ek6Rn4o12/BIKTWgGU0miB9ENcXE19H5D4DRhwG5+4+0zp2U6hRLf/mE73+mRYin7iKVzcwwEsqs+u8w==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-demo@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-+KDUOgcvnMtBN/uYWlhIFuWkTJexuxstq8ERy9q7vOiu8Go85qCb27h0RSToKBTmmGy+XqfU2EdJclYPWBupJQ==} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-figure@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-3nfcGI+uM0f6AqHZrEr8kSMBI6T2+fKKQXtCbvWQqQ+P3iGgf34Ay2eAtuMDcDGqyfNuR6e8aLoOeY2QWuEynA==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-footnote@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-9B+bJdMndCPoA9De9bxRm4/fyz02PHRcttOyuyPJ3G+wCAgIN1c/7CB8ViT1YJuECUjLogJQ/rrgqh7f0LTqLQ==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-img-lazyload@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-6R42ieXzwkB5BKKZi+ZefqeP/fBG5qo7Sqtl72ewSVqEQ30bgxpk6nkrPI2orRob4tb6z0F/c+R8h6PW5MkTOw==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-img-mark@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-HkIUwlTg/xPsBi4PG+5dsMnsb7wdiJzELSCEUfdAJTg55nksonHfyV2pFpr87MML4nuZlZK9JHt+Bm2BBDSVSw==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-img-size@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-fCcF5gc+ba6gQ5ebrKuI8bK/gFbj8mbeN45FHmBsFDFsfTHa0Xij2v8iok0nP8YEIVj71y8XYojsqCWs6avong==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-include@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-8pnmp7s1TjbtoBIa/YhYpEivOpeVSyhkQoQrGq1UoaEcTbXqmFwShGkAW3zUYZVFYTl74PgL/UqJnrUojegJQg==} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - upath: 2.0.1 - dev: true - - /@mdit/plugin-katex-slim@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-s2MJGXFZT7u8IUTmy6K1rxxAdYRmGggu0m860siyUrThL112xLN9r3jmXZ83epgi4UA/gLkRDAU5vF6R2JtyjQ==} - engines: {node: '>= 18'} - peerDependencies: - katex: ^0.16.9 - markdown-it: ^14.1.0 - peerDependenciesMeta: - katex: - optional: true - markdown-it: - optional: true - dependencies: - '@mdit/plugin-tex': 0.12.0(markdown-it@14.1.0) - '@types/katex': 0.16.7 - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-mark@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-BDFwbV/tbgUGL8KF2ymYNLEXT2KNBLe8D0rshDrbB4Iko1U2DywACQkmaUbYBJ1VCn7/dff35at9fWrm3QjrwQ==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-mathjax-slim@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-bLM+JnCTN/3XiyKb64Yhpx014VYLfHBexua4n92cUyoKR9g3waB0loF1WMlg6GdyCTc7OvrUSceNjwWj3YRogg==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - mathjax-full: ^3.2.2 - peerDependenciesMeta: - markdown-it: - optional: true - mathjax-full: - optional: true - dependencies: - '@mdit/plugin-tex': 0.12.0(markdown-it@14.1.0) - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - upath: 2.0.1 - dev: true - - /@mdit/plugin-plantuml@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-m1pk6PA9+kWUs8kylLqjnQ7Lex68x3c4Ato8zAh+omkhugfWzuQXfFiXRiJ9C7wkdqHoJx/E5XobP3HJnhCpoA==} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@mdit/plugin-uml': 0.12.0(markdown-it@14.1.0) - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-spoiler@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-7yu+Gz000O0OxGnGYOoj77Am3WgH4GwzOvwCp7tPLexkJwTve8MyT9In/NEPFaRw8fmgXwthC0gKq4Ubh1+8DA==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-stylize@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-5bzZvmjEpGTdwBax9jaDbCBhD1snEx6uTHVUG9HD/L5koKrL86+ox9E5FGeiMiD1dtxeMgL+WqBzV44nRE9ZPg==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-sub@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-27kKkSVkymc+2RNc5XOYkeXip5PgHZPUnHpxUvkpnairLwyHsXb8/gzr9zd5arVkip86rcdy9LIvnF7zO0dNVQ==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-sup@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-3bEDW5/y1UDVU8LVbFsqUvNcMW6orp16uCdRGYCNZ3/IeK7Qj1/9a3wfhScIoI8xRUE6M3JLv41sGBFXLHwi1w==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-tab@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-ZDTEDxHoekcFA5Al+NLizn8Nf0kj6ABkNBAc/VxbQoVQdjZNQtGY2dOPeWW0I96Rao+Aw+IpYRCLFIfb/KtExw==} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-tasklist@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-MPmuLJrqHYR2xI7ST9Xtw/xj+6Xoq7kUvcGuXWdMMNT11DcU1KppkR8QBHov437NFYh6aGyjrHUVeM4T5Ls8yg==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-tex@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-ejeSgSeZvcI5P4hFFQ4q5pHrZBGO2fQWVGm6dZ3BhX4ldoV8LjCIzkcMMXhrhSOVjwHnqmF6xOh9EvI0jzak1w==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@mdit/plugin-uml@0.12.0(markdown-it@14.1.0): - resolution: {integrity: sha512-EfVMmq0CwLJcssxhkvGS2ESenNNEMeK04j702Z9v3am1M9DdEj6zHTrHQd9tA0jNVuFY8ZlmMgDfkkG5k6Rm3Q==} - engines: {node: '>= 18'} - peerDependencies: - markdown-it: ^14.1.0 - peerDependenciesMeta: - markdown-it: - optional: true - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true - - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true - - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 - dev: true - - /@rollup/rollup-android-arm-eabi@4.20.0: - resolution: {integrity: sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-android-arm64@4.20.0: - resolution: {integrity: sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-arm64@4.20.0: - resolution: {integrity: sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-x64@4.20.0: - resolution: {integrity: sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm-gnueabihf@4.20.0: - resolution: {integrity: sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm-musleabihf@4.20.0: - resolution: {integrity: sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-gnu@4.20.0: - resolution: {integrity: sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-musl@4.20.0: - resolution: {integrity: sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-powerpc64le-gnu@4.20.0: - resolution: {integrity: sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-riscv64-gnu@4.20.0: - resolution: {integrity: sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-s390x-gnu@4.20.0: - resolution: {integrity: sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-gnu@4.20.0: - resolution: {integrity: sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-musl@4.20.0: - resolution: {integrity: sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-arm64-msvc@4.20.0: - resolution: {integrity: sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-ia32-msvc@4.20.0: - resolution: {integrity: sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-x64-msvc@4.20.0: - resolution: {integrity: sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@sec-ant/readable-stream@0.4.1: - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - dev: true - - /@sindresorhus/merge-streams@2.3.0: - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} - dev: true - - /@sindresorhus/merge-streams@4.0.0: - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} - dev: true - - /@types/debug@4.1.12: - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - dependencies: - '@types/ms': 0.7.34 - dev: true - - /@types/estree@1.0.5: - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - dev: true - - /@types/fs-extra@11.0.4: - resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} - dependencies: - '@types/jsonfile': 6.1.4 - '@types/node': 22.3.0 - dev: true - - /@types/hash-sum@1.0.2: - resolution: {integrity: sha512-UP28RddqY8xcU0SCEp9YKutQICXpaAq9N8U2klqF5hegGha7KzTOL8EdhIIV3bOSGBzjEpN9bU/d+nNZBdJYVw==} - dev: true - - /@types/jsonfile@6.1.4: - resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - dependencies: - '@types/node': 22.3.0 - dev: true - - /@types/katex@0.16.7: - resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} - dev: true - - /@types/linkify-it@3.0.5: - resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} - dev: true - - /@types/linkify-it@5.0.0: - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - dev: true - - /@types/markdown-it-emoji@2.0.5: - resolution: {integrity: sha512-iJLsmCNpSWKtV6Ia3mLSjcXJPEt7ubGG342z+hGvYx++TpM19oTUrJcI7XjbOqRQ+W2UQ323E7B0eCLwlgT/9g==} - dependencies: - '@types/markdown-it': 13.0.9 - dev: true - - /@types/markdown-it-emoji@3.0.1: - resolution: {integrity: sha512-cz1j8R35XivBqq9mwnsrP2fsz2yicLhB8+PDtuVkKOExwEdsVBNI+ROL3sbhtR5occRZ66vT0QnwFZCqdjf3pA==} - dependencies: - '@types/markdown-it': 14.1.2 - dev: true - - /@types/markdown-it@13.0.9: - resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} - dependencies: - '@types/linkify-it': 3.0.5 - '@types/mdurl': 1.0.5 - dev: true - - /@types/markdown-it@14.1.2: - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - dev: true - - /@types/mdurl@1.0.5: - resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} - dev: true - - /@types/mdurl@2.0.0: - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - dev: true - - /@types/ms@0.7.34: - resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} - dev: true - - /@types/node@17.0.45: - resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - dev: true - - /@types/node@22.3.0: - resolution: {integrity: sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==} - dependencies: - undici-types: 6.18.2 - dev: true - - /@types/sax@1.2.7: - resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} - dependencies: - '@types/node': 17.0.45 - dev: true - - /@types/web-bluetooth@0.0.20: - resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} - dev: true - - /@vitejs/plugin-vue@5.1.2(vite@5.0.13)(vue@3.4.38): - resolution: {integrity: sha512-nY9IwH12qeiJqumTCLJLE7IiNx7HZ39cbHaysEUd+Myvbz9KAqd2yq+U01Kab1R/H1BmiyM2ShTYlNH32Fzo3A==} - engines: {node: ^18.0.0 || >=20.0.0} - peerDependencies: - vite: ^5.0.0 - vue: ^3.2.25 - dependencies: - vite: 5.0.13 - vue: 3.4.38 - dev: true - - /@vue/compiler-core@3.4.38: - resolution: {integrity: sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==} - dependencies: - '@babel/parser': 7.25.3 - '@vue/shared': 3.4.38 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.0 - dev: true - - /@vue/compiler-core@3.5.8: - resolution: {integrity: sha512-Uzlxp91EPjfbpeO5KtC0KnXPkuTfGsNDeaKQJxQN718uz+RqDYarEf7UhQJGK+ZYloD2taUbHTI2J4WrUaZQNA==} - dependencies: - '@babel/parser': 7.25.3 - '@vue/shared': 3.5.8 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.0 - dev: true - - /@vue/compiler-dom@3.4.38: - resolution: {integrity: sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==} - dependencies: - '@vue/compiler-core': 3.4.38 - '@vue/shared': 3.4.38 - dev: true - - /@vue/compiler-dom@3.5.8: - resolution: {integrity: sha512-GUNHWvoDSbSa5ZSHT9SnV5WkStWfzJwwTd6NMGzilOE/HM5j+9EB9zGXdtu/fCNEmctBqMs6C9SvVPpVPuk1Eg==} - dependencies: - '@vue/compiler-core': 3.5.8 - '@vue/shared': 3.5.8 - dev: true - - /@vue/compiler-sfc@3.4.38: - resolution: {integrity: sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==} - dependencies: - '@babel/parser': 7.25.3 - '@vue/compiler-core': 3.4.38 - '@vue/compiler-dom': 3.4.38 - '@vue/compiler-ssr': 3.4.38 - '@vue/shared': 3.4.38 - estree-walker: 2.0.2 - magic-string: 0.30.11 - postcss: 8.4.41 - source-map-js: 1.2.0 - dev: true - - /@vue/compiler-sfc@3.5.8: - resolution: {integrity: sha512-taYpngQtSysrvO9GULaOSwcG5q821zCoIQBtQQSx7Uf7DxpR6CIHR90toPr9QfDD2mqHQPCSgoWBvJu0yV9zjg==} - dependencies: - '@babel/parser': 7.25.3 - '@vue/compiler-core': 3.5.8 - '@vue/compiler-dom': 3.5.8 - '@vue/compiler-ssr': 3.5.8 - '@vue/shared': 3.5.8 - estree-walker: 2.0.2 - magic-string: 0.30.11 - postcss: 8.4.47 - source-map-js: 1.2.0 - dev: true - - /@vue/compiler-ssr@3.4.38: - resolution: {integrity: sha512-YXznKFQ8dxYpAz9zLuVvfcXhc31FSPFDcqr0kyujbOwNhlmaNvL2QfIy+RZeJgSn5Fk54CWoEUeW+NVBAogGaw==} - dependencies: - '@vue/compiler-dom': 3.4.38 - '@vue/shared': 3.4.38 - dev: true - - /@vue/compiler-ssr@3.5.8: - resolution: {integrity: sha512-W96PtryNsNG9u0ZnN5Q5j27Z/feGrFV6zy9q5tzJVyJaLiwYxvC0ek4IXClZygyhjm+XKM7WD9pdKi/wIRVC/Q==} - dependencies: - '@vue/compiler-dom': 3.5.8 - '@vue/shared': 3.5.8 - dev: true - - /@vue/devtools-api@6.6.3: - resolution: {integrity: sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==} - dev: true - - /@vue/reactivity@3.4.38: - resolution: {integrity: sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==} - dependencies: - '@vue/shared': 3.4.38 - dev: true - - /@vue/reactivity@3.5.8: - resolution: {integrity: sha512-mlgUyFHLCUZcAYkqvzYnlBRCh0t5ZQfLYit7nukn1GR96gc48Bp4B7OIcSfVSvlG1k3BPfD+p22gi1t2n9tsXg==} - dependencies: - '@vue/shared': 3.5.8 - dev: true - - /@vue/runtime-core@3.4.38: - resolution: {integrity: sha512-21z3wA99EABtuf+O3IhdxP0iHgkBs1vuoCAsCKLVJPEjpVqvblwBnTj42vzHRlWDCyxu9ptDm7sI2ZMcWrQqlA==} - dependencies: - '@vue/reactivity': 3.4.38 - '@vue/shared': 3.4.38 - dev: true - - /@vue/runtime-core@3.5.8: - resolution: {integrity: sha512-fJuPelh64agZ8vKkZgp5iCkPaEqFJsYzxLk9vSC0X3G8ppknclNDr61gDc45yBGTaN5Xqc1qZWU3/NoaBMHcjQ==} - dependencies: - '@vue/reactivity': 3.5.8 - '@vue/shared': 3.5.8 - dev: true - - /@vue/runtime-dom@3.4.38: - resolution: {integrity: sha512-afZzmUreU7vKwKsV17H1NDThEEmdYI+GCAK/KY1U957Ig2NATPVjCROv61R19fjZNzMmiU03n79OMnXyJVN0UA==} - dependencies: - '@vue/reactivity': 3.4.38 - '@vue/runtime-core': 3.4.38 - '@vue/shared': 3.4.38 - csstype: 3.1.3 - dev: true - - /@vue/runtime-dom@3.5.8: - resolution: {integrity: sha512-DpAUz+PKjTZPUOB6zJgkxVI3GuYc2iWZiNeeHQUw53kdrparSTG6HeXUrYDjaam8dVsCdvQxDz6ZWxnyjccUjQ==} - dependencies: - '@vue/reactivity': 3.5.8 - '@vue/runtime-core': 3.5.8 - '@vue/shared': 3.5.8 - csstype: 3.1.3 - dev: true - - /@vue/server-renderer@3.4.38(vue@3.4.38): - resolution: {integrity: sha512-NggOTr82FbPEkkUvBm4fTGcwUY8UuTsnWC/L2YZBmvaQ4C4Jl/Ao4HHTB+l7WnFCt5M/dN3l0XLuyjzswGYVCA==} - peerDependencies: - vue: 3.4.38 - dependencies: - '@vue/compiler-ssr': 3.4.38 - '@vue/shared': 3.4.38 - vue: 3.4.38 - dev: true - - /@vue/server-renderer@3.5.8(vue@3.5.8): - resolution: {integrity: sha512-7AmC9/mEeV9mmXNVyUIm1a1AjUhyeeGNbkLh39J00E7iPeGks8OGRB5blJiMmvqSh8SkaS7jkLWSpXtxUCeagA==} - peerDependencies: - vue: 3.5.8 - dependencies: - '@vue/compiler-ssr': 3.5.8 - '@vue/shared': 3.5.8 - vue: 3.5.8 - dev: true - - /@vue/shared@3.4.38: - resolution: {integrity: sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==} - dev: true - - /@vue/shared@3.5.8: - resolution: {integrity: sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A==} - dev: true - - /@vuepress/bundler-vite@2.0.0-rc.7: - resolution: {integrity: sha512-2jNnU3sgHJuUCPfE0DvGRsAxo/A/Locguvnv4Q6QwJYdB1fuAqE6x5p8RDzq8Lv/GsyeexDWiV/PFER5EtVP3w==} - dependencies: - '@vitejs/plugin-vue': 5.1.2(vite@5.0.13)(vue@3.4.38) - '@vuepress/client': 2.0.0-rc.7 - '@vuepress/core': 2.0.0-rc.7 - '@vuepress/shared': 2.0.0-rc.7 - '@vuepress/utils': 2.0.0-rc.7 - autoprefixer: 10.4.20(postcss@8.4.41) - connect-history-api-fallback: 2.0.0 - postcss: 8.4.41 - postcss-load-config: 5.1.0(postcss@8.4.41) - rollup: 4.20.0 - vite: 5.0.13 - vue: 3.4.38 - vue-router: 4.4.3(vue@3.4.38) - transitivePeerDependencies: - - '@types/node' - - '@vue/composition-api' - - jiti - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - - tsx - - typescript - dev: true - - /@vuepress/cli@2.0.0-rc.15: - resolution: {integrity: sha512-frJ0G4sOWUicpa6P1avpgYT0ZLfaAPeRCugghaQGVPnssYuddsP9KBIwfsJ5RP4lWpwfcsdZEuJKKrVJDh6PkQ==} - hasBin: true - dependencies: - '@vuepress/core': 2.0.0-rc.15 - '@vuepress/shared': 2.0.0-rc.15 - '@vuepress/utils': 2.0.0-rc.15 - cac: 6.7.14 - chokidar: 3.6.0 - envinfo: 7.13.0 - esbuild: 0.21.5 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@vuepress/client@2.0.0-rc.15: - resolution: {integrity: sha512-73TWRIFKFqDRI+JHFjVWUJNfeExBYUxR7QuB5ZrtbntvN7Tu+Qr6wMwTU9bwDttRIaxUzoTBuTN5wfAiijokHg==} - dependencies: - '@vue/devtools-api': 6.6.3 - '@vuepress/shared': 2.0.0-rc.15 - vue: 3.4.38 - vue-router: 4.4.3(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/client@2.0.0-rc.7: - resolution: {integrity: sha512-T8jf9h8dfP1ln/7uOIiTAJrdyKRTyq1x+RHXuc7GoRxTfOw+bIYgGvh63Z7m1e3K/yh1nO9aM1WcXbDN1Swp4w==} - dependencies: - '@vue/devtools-api': 6.6.3 - '@vuepress/shared': 2.0.0-rc.7 - '@vueuse/core': 10.11.1(vue@3.4.38) - vue: 3.4.38 - vue-router: 4.4.3(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - dev: true - - /@vuepress/core@2.0.0-rc.15: - resolution: {integrity: sha512-CcVuANpF3k5QH6k27uShpQc+fdb35QdMbqNr4YEBleCVM2M5fHPeA8Did2OgzDzdr15G+AX2aP98yNVLgNR8cw==} - dependencies: - '@vuepress/client': 2.0.0-rc.15 - '@vuepress/markdown': 2.0.0-rc.15 - '@vuepress/shared': 2.0.0-rc.15 - '@vuepress/utils': 2.0.0-rc.15 - vue: 3.4.38 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@vuepress/core@2.0.0-rc.7: - resolution: {integrity: sha512-SPd2C9MIwHLe4mEWJfbcTHT5caS/vW+oeP79wAW0otSGgn2uY3Mmu5qcYPzl+17o8EYv7vDvPiO+uitp/Cek+A==} - dependencies: - '@vuepress/client': 2.0.0-rc.7 - '@vuepress/markdown': 2.0.0-rc.7 - '@vuepress/shared': 2.0.0-rc.7 - '@vuepress/utils': 2.0.0-rc.7 - vue: 3.4.38 - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - - typescript - dev: true - - /@vuepress/helper@2.0.0-rc.39(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-X9KiTUjtrT6gxrDUDJhiB5+/kO4via8yzudowOPu55p/MKtPbShlJw/zEDivH3P4nD1LFWnjWWuEBgZLFymLFQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vue/shared': 3.4.38 - cheerio: 1.0.0-rc.12 - fflate: 0.8.2 - gray-matter: 4.0.3 - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/helper@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-kvjDPSTRiILdUa8HXyV44b7dN5gw38bE4KvnpQTwXIApwDnNCv+LY34cTL6UTOEQTjx9UyVUVkWm2/LGCVLidQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vue/shared': 3.4.38 - cheerio: 1.0.0 - fflate: 0.8.2 - gray-matter: 4.0.3 - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/helper@2.0.0-rc.42(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-ohdf2SALgykvgoN78+IJ4ogmB97Z4msUg8f/KzBEeW3orZLnRnzl8oK9AWqeU4wS35rKDKjxlqp8iFKnnoy/fw==} - peerDependencies: - vuepress: 2.0.0-rc.15 - dependencies: - '@vue/shared': 3.4.38 - cheerio: 1.0.0 - fflate: 0.8.2 - gray-matter: 4.0.3 - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/helper@2.0.0-rc.46(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-LofxIcQk9584cSICl9DwXpfZ0EqUe5nC9u01+edZde8JSNLjncMx2rFfFKUl2YpuSCZpCkL71JgZ3Ym/1AUfaA==} - peerDependencies: - vuepress: 2.0.0-rc.15 - dependencies: - '@vue/shared': 3.5.8 - '@vueuse/core': 11.1.0(vue@3.5.8) - cheerio: 1.0.0 - fflate: 0.8.2 - gray-matter: 4.0.3 - vue: 3.5.8 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - dev: true - - /@vuepress/highlighter-helper@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-MaZhWpjL2baw1+IyBRUcH84rIA/qx6FxoHpfvxr90VqNjCSnjJZ09zY+1vXAdvie0Z5rMHEmziO+eX0DHmWQzw==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - dev: true - - /@vuepress/markdown@2.0.0-rc.15: - resolution: {integrity: sha512-q+yuwV+PzvFCs92Q/O/3HHRDY9CNYKJgc+fKbqE98P3qGBATnDtABUjc+NjtPvG4OdYt2fELTXOzyprE8omFIw==} - dependencies: - '@mdit-vue/plugin-component': 2.1.3 - '@mdit-vue/plugin-frontmatter': 2.1.3 - '@mdit-vue/plugin-headers': 2.1.3 - '@mdit-vue/plugin-sfc': 2.1.3 - '@mdit-vue/plugin-title': 2.1.3 - '@mdit-vue/plugin-toc': 2.1.3 - '@mdit-vue/shared': 2.1.3 - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 14.1.2 - '@types/markdown-it-emoji': 3.0.1 - '@vuepress/shared': 2.0.0-rc.15 - '@vuepress/utils': 2.0.0-rc.15 - markdown-it: 14.1.0 - markdown-it-anchor: 9.0.1(@types/markdown-it@14.1.2)(markdown-it@14.1.0) - markdown-it-emoji: 3.0.0 - mdurl: 2.0.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@vuepress/markdown@2.0.0-rc.7: - resolution: {integrity: sha512-mczvo7MZxxXXj5htDXK22r0a7JjTP5sTlcywtVOTJurzCKp8SNSbr3HdmsAnr9S838Hn9+dkvhs57rqgPG3UHA==} - dependencies: - '@mdit-vue/plugin-component': 2.1.3 - '@mdit-vue/plugin-frontmatter': 2.1.3 - '@mdit-vue/plugin-headers': 2.1.3 - '@mdit-vue/plugin-sfc': 2.1.3 - '@mdit-vue/plugin-title': 2.1.3 - '@mdit-vue/plugin-toc': 2.1.3 - '@mdit-vue/shared': 2.1.3 - '@mdit-vue/types': 2.1.0 - '@types/markdown-it': 13.0.9 - '@types/markdown-it-emoji': 2.0.5 - '@vuepress/shared': 2.0.0-rc.7 - '@vuepress/utils': 2.0.0-rc.7 - markdown-it: 14.1.0 - markdown-it-anchor: 8.6.7(@types/markdown-it@13.0.9)(markdown-it@14.1.0) - markdown-it-emoji: 3.0.0 - mdurl: 2.0.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@vuepress/plugin-active-header-links@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-Xs86rPsJZKR5+s5fdMXbMbwr2aq25Evf7jA/AG6osds0/qwaqQSH4j4Qp49TZULCVXtcK4M3DfEYnRlbigntrQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vueuse/core': 10.11.1(vue@3.4.38) - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - dev: true - - /@vuepress/plugin-back-to-top@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-wx23ZdQsaikgREiCWoIXSdaM5310zU8MkZaQhep2jV54OR0KR8u1+57ZBZxwbjSV9jdkGyxkXSxbqZGCFh1KZg==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vuepress/helper': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vueuse/core': 10.11.1(vue@3.4.38) - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - dev: true - - /@vuepress/plugin-copy-code@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-QzYSEkIlIvAPPAQ4dIicH+hpXjzK4l+UW/7ATdOt1kTVEr5+e1hprB0xFseix6WeoWMsSQLePybz6s1JxCHtIA==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vuepress/helper': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vueuse/core': 10.11.1(vue@3.4.38) - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - dev: true - - /@vuepress/plugin-docsearch@2.0.0-rc.42(@algolia/client-search@5.4.0)(search-insights@2.17.2)(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-aFyUvFayTnvx2x8tr3QamgJT/UzeF4nOi308aWYw263lmhs8nKLwgioR1bSisArApmMzGgP5u8ozuL24Ouyz1w==} - peerDependencies: - vuepress: 2.0.0-rc.15 - dependencies: - '@docsearch/css': 3.6.1 - '@docsearch/js': 3.6.1(@algolia/client-search@5.4.0)(search-insights@2.17.2) - '@docsearch/react': 3.6.1(@algolia/client-search@5.4.0)(search-insights@2.17.2) - '@vuepress/helper': 2.0.0-rc.42(vuepress@2.0.0-rc.15) - '@vueuse/core': 11.0.3(vue@3.4.38) - ts-debounce: 4.0.0 - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - '@algolia/client-search' - - '@types/react' - - '@vue/composition-api' - - react - - react-dom - - search-insights - - typescript - dev: true - - /@vuepress/plugin-git@2.0.0-rc.38(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-dRJiZ5PVuhhyu+R2BZOlyeqgxVikUUh2Vf6RNVN2DNWv4VHdYybFQuQ+kYDpldYyzoP8932aFRV0d2ocpvxEug==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - execa: 9.3.1 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - dev: true - - /@vuepress/plugin-links-check@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-qGYw97Q7bq5H2fukx+nZ4ReSxyEJl82VGL6l51jh8Mx5g0/ge7aGc5Yu2kv84T4S1+PvYSU5JAPiQIZ3H3EISQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vuepress/helper': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/plugin-markdown-container@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-mOyvh4WqBI5cQMBKyAzP0CcYsFNYbMl7ATyNeVMM+2nbBfDyTL8EdFVvYwxuu3ezF7hrFfbzm2O2aqUbIByF4Q==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it-container: 4.0.0 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - dev: true - - /@vuepress/plugin-medium-zoom@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-/Drv+z0Y6yNig1feK6Fyz8hySWfM9ijKhgHP2nsVNrtC3YwraUZDdFz8w4Lj7VurQwyf+yfg78ODUdSmUi5ScQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vuepress/helper': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - medium-zoom: 1.1.0 - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/plugin-nprogress@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-AQZJUdeX9clyMJGLiOvEo0du2XJI5/QMV/emPPXtwInm2OugskWGIOovcCkA/c7UrLFsHuTQOZGMtcNcViV4vQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/plugin-palette@2.0.0-rc.37(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-wzsywEeZFHOcRITIff1SC0jXsMasze3t0d9gc0cp5iB/OW3bEdhes4mFmPtZZmWfnlmgajq/7uTEavEXVWJTIA==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - chokidar: 3.6.0 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - dev: true - - /@vuepress/plugin-prismjs@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-0d9LSi5kpZFnAw4ISUDCpQKjUIB1gemjlZvw4SdV8b+dttl1dC8Ykrm93WNiVeR8gWhS+8Oa7J92Kd23ZNlFeQ==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vuepress/helper': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/highlighter-helper': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - prismjs: 1.29.0 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/plugin-redirect@2.0.0-rc.42(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-W5oSWfmNi+4xP079leb/kKvT3KOgIuDR1pdbhMzx9cW+ZEmMv/XmMR42q42crDPEB4FJGV4tViTBm9pk1+KeJg==} - hasBin: true - peerDependencies: - vuepress: 2.0.0-rc.15 - dependencies: - '@vuepress/helper': 2.0.0-rc.42(vuepress@2.0.0-rc.15) - '@vueuse/core': 11.0.3(vue@3.4.38) - cac: 6.7.14 - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - dev: true - - /@vuepress/plugin-sass-palette@2.0.0-rc.39(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-jBo/4Lz7H9pa7TCqMSFiEyZRowsLCOVHj+yrp0PE1fAwx+qTm9dNSDKauWIKGplFGQqf4BdKITE7hPDoBePiDw==} - peerDependencies: - sass-loader: ^14.0.0 - vuepress: 2.0.0-rc.14 - peerDependenciesMeta: - sass-loader: - optional: true - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.15) - chokidar: 3.6.0 - sass: 1.77.8 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/plugin-seo@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-X2UT4MrPddkZlBs12KIRCayQuFG9GNv0xi48vHKZAWWdx0kw+LVnM0VwwqhuhQOJgK2Rrfv4SCGXZcsSzoZsYg==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vuepress/helper': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/plugin-seo@2.0.0-rc.46(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-RxZBMN6xHjoYk5tmsIatsEebk2u6kDldQnkjMkjRrX4UkC+e/rd+ehqIUUEba6dauPCFkI1tV3eXht5WOsx5ZA==} - peerDependencies: - vuepress: 2.0.0-rc.15 - dependencies: - '@vuepress/helper': 2.0.0-rc.46(vuepress@2.0.0-rc.15) - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - dev: true - - /@vuepress/plugin-sitemap@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-l9QkuKKh0OvMXwF0jw+PR/2ainCtiS13l6/3oOemfAfGR7X5au2rBjuASCYljy+PmKLaXEJnKEh+kkKdx0cP9Q==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vuepress/helper': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - sitemap: 8.0.0 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/plugin-theme-data@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-ts6o+NSlBU+DeAu85XvQdbEGbVwBOgEubOwvB/ZFRqQN3o62mhzoW7OoyPme/KGGyztDnpDHhM3NqsgNAtF8gw==} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vue/devtools-api': 6.6.3 - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - typescript - dev: true - - /@vuepress/shared@2.0.0-rc.15: - resolution: {integrity: sha512-KLTTw0MWXrLw5XQ8h6y1iI11zJdlZcB8jXgAdOHFJ3a++InDYmUC0aCE0KbXyQTH46ODpMadJnz8/RewCftVXg==} - dependencies: - '@mdit-vue/types': 2.1.0 - dev: true - - /@vuepress/shared@2.0.0-rc.7: - resolution: {integrity: sha512-zNsYzAW5tuENb4vML0pK/61W3EscyOcn5JVPC+c6AwvsYWyAigZaMSu9ycGAptjDwzdYSi3gd33N9Q9T7pG77Q==} - dependencies: - '@mdit-vue/types': 2.1.0 - dev: true - - /@vuepress/theme-default@2.0.0-rc.41(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-kYWZsR+8G3lDaIgPxBsqSzcWTzY5U9UZ2tRgCCKiI+7xeFghg1uunIMJ0nfIxiPlVA18DH9grJPFC8BPimE88g==} - peerDependencies: - sass-loader: ^16.0.0 - vuepress: 2.0.0-rc.14 - peerDependenciesMeta: - sass-loader: - optional: true - dependencies: - '@vuepress/helper': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/plugin-active-header-links': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/plugin-back-to-top': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/plugin-copy-code': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/plugin-git': 2.0.0-rc.38(vuepress@2.0.0-rc.15) - '@vuepress/plugin-links-check': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/plugin-markdown-container': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/plugin-medium-zoom': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/plugin-nprogress': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/plugin-palette': 2.0.0-rc.37(vuepress@2.0.0-rc.15) - '@vuepress/plugin-prismjs': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/plugin-seo': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/plugin-sitemap': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vuepress/plugin-theme-data': 2.0.0-rc.41(vuepress@2.0.0-rc.15) - '@vueuse/core': 10.11.1(vue@3.4.38) - sass: 1.77.8 - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - dev: true - - /@vuepress/utils@2.0.0-rc.15: - resolution: {integrity: sha512-yHdMxPg8yxlAz/QL5G7U1IQ+jk34sCtn8H8X5j+A2U1x/j2lnr8LLDWzl84TyN78pdEA1cQL5g7Ks/zTzrIWhQ==} - dependencies: - '@types/debug': 4.1.12 - '@types/fs-extra': 11.0.4 - '@types/hash-sum': 1.0.2 - '@vuepress/shared': 2.0.0-rc.15 - debug: 4.3.6 - fs-extra: 11.2.0 - globby: 14.0.2 - hash-sum: 2.0.0 - ora: 8.0.1 - picocolors: 1.0.1 - upath: 2.0.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@vuepress/utils@2.0.0-rc.7: - resolution: {integrity: sha512-47c7T72JwOWH1EgG7f/KwWNpIknd9IC5JmrizGI5IVUM4G16Vyj5oPQuXqLobV47m8JOxPW4dLpsglZGVaKQeg==} - dependencies: - '@types/debug': 4.1.12 - '@types/fs-extra': 11.0.4 - '@types/hash-sum': 1.0.2 - '@vuepress/shared': 2.0.0-rc.7 - debug: 4.3.6 - fs-extra: 11.2.0 - globby: 14.0.2 - hash-sum: 2.0.0 - ora: 8.0.1 - picocolors: 1.0.1 - upath: 2.0.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@vueuse/core@10.11.1(vue@3.4.38): - resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} - dependencies: - '@types/web-bluetooth': 0.0.20 - '@vueuse/metadata': 10.11.1 - '@vueuse/shared': 10.11.1(vue@3.4.38) - vue-demi: 0.14.10(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - dev: true - - /@vueuse/core@11.0.3(vue@3.4.38): - resolution: {integrity: sha512-RENlh64+SYA9XMExmmH1a3TPqeIuJBNNB/63GT35MZI+zpru3oMRUA6cEFr9HmGqEgUisurwGwnIieF6qu3aXw==} - dependencies: - '@types/web-bluetooth': 0.0.20 - '@vueuse/metadata': 11.0.3 - '@vueuse/shared': 11.0.3(vue@3.4.38) - vue-demi: 0.14.10(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - dev: true - - /@vueuse/core@11.1.0(vue@3.5.8): - resolution: {integrity: sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg==} - dependencies: - '@types/web-bluetooth': 0.0.20 - '@vueuse/metadata': 11.1.0 - '@vueuse/shared': 11.1.0(vue@3.5.8) - vue-demi: 0.14.10(vue@3.5.8) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - dev: true - - /@vueuse/metadata@10.11.1: - resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} - dev: true - - /@vueuse/metadata@11.0.3: - resolution: {integrity: sha512-+FtbO4SD5WpsOcQTcC0hAhNlOid6QNLzqedtquTtQ+CRNBoAt9GuV07c6KNHK1wCmlq8DFPwgiLF2rXwgSHX5Q==} - dev: true - - /@vueuse/metadata@11.1.0: - resolution: {integrity: sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg==} - dev: true - - /@vueuse/shared@10.11.1(vue@3.4.38): - resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} - dependencies: - vue-demi: 0.14.10(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - dev: true - - /@vueuse/shared@11.0.3(vue@3.4.38): - resolution: {integrity: sha512-0rY2m6HS5t27n/Vp5cTDsKTlNnimCqsbh/fmT2LgE+aaU42EMfXo8+bNX91W9I7DDmxfuACXMmrd7d79JxkqWA==} - dependencies: - vue-demi: 0.14.10(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - dev: true - - /@vueuse/shared@11.1.0(vue@3.5.8): - resolution: {integrity: sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==} - dependencies: - vue-demi: 0.14.10(vue@3.5.8) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - dev: true - - /algoliasearch@4.24.0: - resolution: {integrity: sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==} - dependencies: - '@algolia/cache-browser-local-storage': 4.24.0 - '@algolia/cache-common': 4.24.0 - '@algolia/cache-in-memory': 4.24.0 - '@algolia/client-account': 4.24.0 - '@algolia/client-analytics': 4.24.0 - '@algolia/client-common': 4.24.0 - '@algolia/client-personalization': 4.24.0 - '@algolia/client-search': 4.24.0 - '@algolia/logger-common': 4.24.0 - '@algolia/logger-console': 4.24.0 - '@algolia/recommend': 4.24.0 - '@algolia/requester-browser-xhr': 4.24.0 - '@algolia/requester-common': 4.24.0 - '@algolia/requester-node-http': 4.24.0 - '@algolia/transporter': 4.24.0 - dev: true - - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - dev: true - - /anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: true - - /arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - dev: true - - /argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - dependencies: - sprintf-js: 1.0.3 - dev: true - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true - - /autoprefixer@10.4.20(postcss@8.4.41): - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - dependencies: - browserslist: 4.23.3 - caniuse-lite: 1.0.30001651 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.0.1 - postcss: 8.4.41 - postcss-value-parser: 4.2.0 - dev: true - - /balloon-css@1.2.0: - resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==} - dev: true - - /binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - dev: true - - /boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - dev: true - - /braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.1.1 - dev: true - - /browserslist@4.23.3: - resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001651 - electron-to-chromium: 1.5.8 - node-releases: 2.0.18 - update-browserslist-db: 1.1.0(browserslist@4.23.3) - dev: true - - /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - dev: true - - /caniuse-lite@1.0.30001651: - resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} - dev: true - - /chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: true - - /chart.js@4.4.4: - resolution: {integrity: sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==} - engines: {pnpm: '>=8'} - dependencies: - '@kurkle/color': 0.3.2 - dev: true - - /cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - dependencies: - boolbase: 1.0.0 - css-select: 5.1.0 - css-what: 6.1.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - dev: true - - /cheerio@1.0.0: - resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} - engines: {node: '>=18.17'} - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.1.0 - encoding-sniffer: 0.2.0 - htmlparser2: 9.1.0 - parse5: 7.1.2 - parse5-htmlparser2-tree-adapter: 7.0.0 - parse5-parser-stream: 7.1.2 - undici: 6.19.7 - whatwg-mimetype: 4.0.0 - dev: true - - /cheerio@1.0.0-rc.12: - resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} - engines: {node: '>= 6'} - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.1.0 - htmlparser2: 8.0.2 - parse5: 7.1.2 - parse5-htmlparser2-tree-adapter: 7.0.0 - dev: true - - /chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - restore-cursor: 4.0.0 - dev: true - - /cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - dev: true - - /connect-history-api-fallback@2.0.0: - resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} - engines: {node: '>=0.8'} - dev: true - - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: true - - /css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 5.0.3 - domutils: 3.1.0 - nth-check: 2.1.1 - dev: true - - /css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - dev: true - - /csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - dev: true - - /dayjs@1.11.12: - resolution: {integrity: sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==} - dev: true - - /debug@4.3.6: - resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: true - - /dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - dev: true - - /domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: true - - /domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - dependencies: - domelementtype: 2.3.0 - dev: true - - /domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dev: true - - /electron-to-chromium@1.5.8: - resolution: {integrity: sha512-4Nx0gP2tPNBLTrFxBMHpkQbtn2hidPVr/+/FTtcCiBYTucqc70zRyVZiOLj17Ui3wTO7SQ1/N+hkHYzJjBzt6A==} - dev: true - - /emoji-regex@10.3.0: - resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} - dev: true - - /encoding-sniffer@0.2.0: - resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} - dependencies: - iconv-lite: 0.6.3 - whatwg-encoding: 3.1.1 - dev: true - - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - dev: true - - /envinfo@7.13.0: - resolution: {integrity: sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==} - engines: {node: '>=4'} - hasBin: true - dev: true - - /esbuild@0.19.12: - resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.19.12 - '@esbuild/android-arm': 0.19.12 - '@esbuild/android-arm64': 0.19.12 - '@esbuild/android-x64': 0.19.12 - '@esbuild/darwin-arm64': 0.19.12 - '@esbuild/darwin-x64': 0.19.12 - '@esbuild/freebsd-arm64': 0.19.12 - '@esbuild/freebsd-x64': 0.19.12 - '@esbuild/linux-arm': 0.19.12 - '@esbuild/linux-arm64': 0.19.12 - '@esbuild/linux-ia32': 0.19.12 - '@esbuild/linux-loong64': 0.19.12 - '@esbuild/linux-mips64el': 0.19.12 - '@esbuild/linux-ppc64': 0.19.12 - '@esbuild/linux-riscv64': 0.19.12 - '@esbuild/linux-s390x': 0.19.12 - '@esbuild/linux-x64': 0.19.12 - '@esbuild/netbsd-x64': 0.19.12 - '@esbuild/openbsd-x64': 0.19.12 - '@esbuild/sunos-x64': 0.19.12 - '@esbuild/win32-arm64': 0.19.12 - '@esbuild/win32-ia32': 0.19.12 - '@esbuild/win32-x64': 0.19.12 - dev: true - - /esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - dev: true - - /escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} - engines: {node: '>=6'} - dev: true - - /esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: true - - /estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true - - /execa@9.3.1: - resolution: {integrity: sha512-gdhefCCNy/8tpH/2+ajP9IQc14vXchNdd0weyzSJEFURhRMGncQ+zKFxwjAufIewPEJm9BPOaJnvg2UtlH2gPQ==} - engines: {node: ^18.19.0 || >=20.5.0} - dependencies: - '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.3 - figures: 6.1.0 - get-stream: 9.0.1 - human-signals: 8.0.0 - is-plain-obj: 4.1.0 - is-stream: 4.0.1 - npm-run-path: 5.3.0 - pretty-ms: 9.1.0 - signal-exit: 4.1.0 - strip-final-newline: 4.0.0 - yoctocolors: 2.1.1 - dev: true - - /extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - dependencies: - is-extendable: 0.1.1 - dev: true - - /fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.7 - dev: true - - /fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - dependencies: - reusify: 1.0.4 - dev: true - - /fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - dev: true - - /figures@6.1.0: - resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} - engines: {node: '>=18'} - dependencies: - is-unicode-supported: 2.0.0 - dev: true - - /fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: true - - /fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - dev: true - - /fs-extra@11.2.0: - resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} - engines: {node: '>=14.14'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.1 - dev: true - - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /get-east-asian-width@1.2.0: - resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} - engines: {node: '>=18'} - dev: true - - /get-stream@9.0.1: - resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} - engines: {node: '>=18'} - dependencies: - '@sec-ant/readable-stream': 0.4.1 - is-stream: 4.0.1 - dev: true - - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: true - - /globby@14.0.2: - resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} - engines: {node: '>=18'} - dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.2 - ignore: 5.3.2 - path-type: 5.0.0 - slash: 5.1.0 - unicorn-magic: 0.1.0 - dev: true - - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: true - - /gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - dev: true - - /hash-sum@2.0.0: - resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} - dev: true - - /htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - entities: 4.5.0 - dev: true - - /htmlparser2@9.1.0: - resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - entities: 4.5.0 - dev: true - - /human-signals@8.0.0: - resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} - engines: {node: '>=18.18.0'} - dev: true - - /iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: true - - /ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - dev: true - - /immutable@4.3.7: - resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} - dev: true - - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - dependencies: - binary-extensions: 2.3.0 - dev: true - - /is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - dev: true - - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true - - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - - /is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} - engines: {node: '>=12'} - dev: true - - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true - - /is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - dev: true - - /is-stream@4.0.1: - resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} - engines: {node: '>=18'} - dev: true - - /is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - dev: true - - /is-unicode-supported@2.0.0: - resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} - engines: {node: '>=18'} - dev: true - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true - - /js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - dev: true - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: true - - /jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - dev: true - - /kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: true - - /lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} - engines: {node: '>=14'} - dev: true - - /linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - dependencies: - uc.micro: 2.1.0 - dev: true - - /log-symbols@6.0.0: - resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} - engines: {node: '>=18'} - dependencies: - chalk: 5.3.0 - is-unicode-supported: 1.3.0 - dev: true - - /magic-string@0.30.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - dev: true - - /markdown-it-anchor@8.6.7(@types/markdown-it@13.0.9)(markdown-it@14.1.0): - resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} - peerDependencies: - '@types/markdown-it': '*' - markdown-it: '*' - dependencies: - '@types/markdown-it': 13.0.9 - markdown-it: 14.1.0 - dev: true - - /markdown-it-anchor@9.0.1(@types/markdown-it@14.1.2)(markdown-it@14.1.0): - resolution: {integrity: sha512-cBt7aAzmkfX8X7FqAe8EBryiKmToXgMQEEMqkXzWCm0toDtfDYIGboKeTKd8cpNJArJtutrf+977wFJTsvNGmQ==} - peerDependencies: - '@types/markdown-it': '*' - markdown-it: '*' - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - dev: true - - /markdown-it-container@4.0.0: - resolution: {integrity: sha512-HaNccxUH0l7BNGYbFbjmGpf5aLHAMTinqRZQAEQbMr2cdD3z91Q6kIo1oUn1CQndkT03jat6ckrdRYuwwqLlQw==} - dev: true - - /markdown-it-emoji@3.0.0: - resolution: {integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg==} - dev: true - - /markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - dev: true - - /mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - dev: true - - /medium-zoom@1.1.0: - resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==} - dev: true - - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: true - - /micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} - engines: {node: '>=8.6'} - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - dev: true - - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: true - - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true - - /nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - - /node-releases@2.0.18: - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} - dev: true - - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - dev: true - - /normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - dev: true - - /npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - path-key: 4.0.0 - dev: true - - /nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - dependencies: - boolbase: 1.0.0 - dev: true - - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - dependencies: - mimic-fn: 2.1.0 - dev: true - - /ora@8.0.1: - resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==} - engines: {node: '>=18'} - dependencies: - chalk: 5.3.0 - cli-cursor: 4.0.0 - cli-spinners: 2.9.2 - is-interactive: 2.0.0 - is-unicode-supported: 2.0.0 - log-symbols: 6.0.0 - stdin-discarder: 0.2.2 - string-width: 7.2.0 - strip-ansi: 7.1.0 - dev: true - - /parse-ms@4.0.0: - resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} - engines: {node: '>=18'} - dev: true - - /parse5-htmlparser2-tree-adapter@7.0.0: - resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} - dependencies: - domhandler: 5.0.3 - parse5: 7.1.2 - dev: true - - /parse5-parser-stream@7.1.2: - resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} - dependencies: - parse5: 7.1.2 - dev: true - - /parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} - dependencies: - entities: 4.5.0 - dev: true - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: true - - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true - - /path-type@5.0.0: - resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} - engines: {node: '>=12'} - dev: true - - /picocolors@1.0.1: - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} - dev: true - - /picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} - dev: true - - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true - - /postcss-load-config@5.1.0(postcss@8.4.41): - resolution: {integrity: sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==} - engines: {node: '>= 18'} - peerDependencies: - jiti: '>=1.21.0' - postcss: '>=8.0.9' - tsx: ^4.8.1 - peerDependenciesMeta: - jiti: - optional: true - postcss: - optional: true - tsx: - optional: true - dependencies: - lilconfig: 3.1.2 - postcss: 8.4.41 - yaml: 2.5.0 - dev: true - - /postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - dev: true - - /postcss@8.4.41: - resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.1 - source-map-js: 1.2.0 - dev: true - - /postcss@8.4.47: - resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.7 - picocolors: 1.1.0 - source-map-js: 1.2.1 - dev: true - - /preact@10.23.2: - resolution: {integrity: sha512-kKYfePf9rzKnxOAKDpsWhg/ysrHPqT+yQ7UW4JjdnqjFIeNUnNcEJvhuA8fDenxAGWzUqtd51DfVg7xp/8T9NA==} - dev: true - - /pretty-ms@9.1.0: - resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} - engines: {node: '>=18'} - dependencies: - parse-ms: 4.0.0 - dev: true - - /prismjs@1.29.0: - resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} - engines: {node: '>=6'} - dev: true - - /punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - dev: true - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true - - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 - dev: true - - /restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - dev: true - - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true - - /rollup@4.20.0: - resolution: {integrity: sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - dependencies: - '@types/estree': 1.0.5 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.20.0 - '@rollup/rollup-android-arm64': 4.20.0 - '@rollup/rollup-darwin-arm64': 4.20.0 - '@rollup/rollup-darwin-x64': 4.20.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.20.0 - '@rollup/rollup-linux-arm-musleabihf': 4.20.0 - '@rollup/rollup-linux-arm64-gnu': 4.20.0 - '@rollup/rollup-linux-arm64-musl': 4.20.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.20.0 - '@rollup/rollup-linux-riscv64-gnu': 4.20.0 - '@rollup/rollup-linux-s390x-gnu': 4.20.0 - '@rollup/rollup-linux-x64-gnu': 4.20.0 - '@rollup/rollup-linux-x64-musl': 4.20.0 - '@rollup/rollup-win32-arm64-msvc': 4.20.0 - '@rollup/rollup-win32-ia32-msvc': 4.20.0 - '@rollup/rollup-win32-x64-msvc': 4.20.0 - fsevents: 2.3.3 - dev: true - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: true - - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true - - /sass@1.77.8: - resolution: {integrity: sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==} - engines: {node: '>=14.0.0'} - hasBin: true - dependencies: - chokidar: 3.6.0 - immutable: 4.3.7 - source-map-js: 1.2.0 - dev: true - - /sax@1.4.1: - resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - dev: true - - /search-insights@2.17.2: - resolution: {integrity: sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g==} - dev: true - - /section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - dev: true - - /semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} - engines: {node: '>=10'} - hasBin: true - dev: true - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - dev: true - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: true - - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true - - /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - dev: true - - /sitemap@8.0.0: - resolution: {integrity: sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A==} - engines: {node: '>=14.0.0', npm: '>=6.0.0'} - hasBin: true - dependencies: - '@types/node': 17.0.45 - '@types/sax': 1.2.7 - arg: 5.0.2 - sax: 1.4.1 - dev: true - - /slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - dev: true - - /source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - dev: true - - /source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - dev: true - - /sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: true - - /stdin-discarder@0.2.2: - resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} - engines: {node: '>=18'} - dev: true - - /string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - dependencies: - emoji-regex: 10.3.0 - get-east-asian-width: 1.2.0 - strip-ansi: 7.1.0 - dev: true - - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - dependencies: - ansi-regex: 6.0.1 - dev: true - - /strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - dev: true - - /strip-final-newline@4.0.0: - resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} - engines: {node: '>=18'} - dev: true - - /to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - dev: true - - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: true - - /ts-debounce@4.0.0: - resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==} - dev: true - - /uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - dev: true - - /undici-types@6.18.2: - resolution: {integrity: sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==} - dev: true - - /undici@6.19.7: - resolution: {integrity: sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==} - engines: {node: '>=18.17'} - dev: true - - /unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} - engines: {node: '>=18'} - dev: true - - /universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - dev: true - - /upath@2.0.1: - resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} - engines: {node: '>=4'} - dev: true - - /update-browserslist-db@1.1.0(browserslist@4.23.3): - resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - dependencies: - browserslist: 4.23.3 - escalade: 3.1.2 - picocolors: 1.0.1 - dev: true - - /vite@5.0.13: - resolution: {integrity: sha512-/9ovhv2M2dGTuA+dY93B9trfyWMDRQw2jdVBhHNP6wr0oF34wG2i/N55801iZIpgUpnHDm4F/FabGQLyc+eOgg==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - esbuild: 0.19.12 - postcss: 8.4.41 - rollup: 4.20.0 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /vue-demi@0.14.10(vue@3.4.38): - resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - dependencies: - vue: 3.4.38 - dev: true - - /vue-demi@0.14.10(vue@3.5.8): - resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - dependencies: - vue: 3.5.8 - dev: true - - /vue-router@4.4.3(vue@3.4.38): - resolution: {integrity: sha512-sv6wmNKx2j3aqJQDMxLFzs/u/mjA9Z5LCgy6BE0f7yFWMjrPLnS/sPNn8ARY/FXw6byV18EFutn5lTO6+UsV5A==} - peerDependencies: - vue: ^3.2.0 - dependencies: - '@vue/devtools-api': 6.6.3 - vue: 3.4.38 - dev: true - - /vue@3.4.38: - resolution: {integrity: sha512-f0ZgN+mZ5KFgVv9wz0f4OgVKukoXtS3nwET4c2vLBGQR50aI8G0cqbFtLlX9Yiyg3LFGBitruPHt2PxwTduJEw==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@vue/compiler-dom': 3.4.38 - '@vue/compiler-sfc': 3.4.38 - '@vue/runtime-dom': 3.4.38 - '@vue/server-renderer': 3.4.38(vue@3.4.38) - '@vue/shared': 3.4.38 - dev: true - - /vue@3.5.8: - resolution: {integrity: sha512-hvuvuCy51nP/1fSRvrrIqTLSvrSyz2Pq+KQ8S8SXCxTWVE0nMaOnSDnSOxV1eYmGfvK7mqiwvd1C59CEEz7dAQ==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@vue/compiler-dom': 3.5.8 - '@vue/compiler-sfc': 3.5.8 - '@vue/runtime-dom': 3.5.8 - '@vue/server-renderer': 3.5.8(vue@3.5.8) - '@vue/shared': 3.5.8 - dev: true - - /vuepress-plugin-md-enhance@2.0.0-rc.52(chart.js@4.4.4)(markdown-it@14.1.0)(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-4lED2FSelBbtBVE5Hon9FQpCgmG1xQbi3+rFrj/Aa1VF+8PTjqFdG4NkdRTx94beOeu/A5jvWp5TFWMbaXD7hA==} - engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} - peerDependencies: - '@types/reveal.js': ^5.0.0 - '@vue/repl': ^4.1.1 - chart.js: ^4.0.0 - echarts: ^5.0.0 - flowchart.ts: ^2.0.0 || ^3.0.0 - katex: ^0.16.0 - kotlin-playground: ^1.23.0 - markmap-lib: ^0.17.0 - markmap-toolbar: ^0.17.0 - markmap-view: ^0.17.0 - mathjax-full: ^3.2.2 - mermaid: ^10.8.0 - reveal.js: ^5.0.0 - sandpack-vue3: ^3.0.0 - sass-loader: ^14.0.0 - vuepress: 2.0.0-rc.14 - peerDependenciesMeta: - '@types/reveal.js': - optional: true - '@vue/repl': - optional: true - chart.js: - optional: true - echarts: - optional: true - flowchart.ts: - optional: true - katex: - optional: true - kotlin-playground: - optional: true - markmap-lib: - optional: true - markmap-toolbar: - optional: true - markmap-view: - optional: true - mathjax-full: - optional: true - mermaid: - optional: true - reveal.js: - optional: true - sandpack-vue3: - optional: true - sass-loader: - optional: true - dependencies: - '@mdit/plugin-alert': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-align': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-attrs': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-container': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-demo': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-figure': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-footnote': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-img-lazyload': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-img-mark': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-img-size': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-include': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-katex-slim': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-mark': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-mathjax-slim': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-plantuml': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-spoiler': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-stylize': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-sub': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-sup': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-tab': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-tasklist': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-tex': 0.12.0(markdown-it@14.1.0) - '@mdit/plugin-uml': 0.12.0(markdown-it@14.1.0) - '@types/markdown-it': 14.1.2 - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.15) - '@vuepress/plugin-sass-palette': 2.0.0-rc.39(vuepress@2.0.0-rc.15) - '@vueuse/core': 10.11.1(vue@3.4.38) - balloon-css: 1.2.0 - chart.js: 4.4.4 - js-yaml: 4.1.0 - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - vuepress-shared: 2.0.0-rc.52(vuepress@2.0.0-rc.15) - transitivePeerDependencies: - - '@vue/composition-api' - - markdown-it - - typescript - dev: true - - /vuepress-plugin-remove-html-extension@1.13.0: - resolution: {integrity: sha512-ef82agY8Yo2H34rSzIQ+ASimwaaun6mFNvAHzQJUpCfjlktpqxwwLALx3luocexoF8TRD0LdS4IM8Ccv+c6Lsw==} - engines: {node: ^16.0.0 || >=18.0.0} - dependencies: - '@vuepress/core': 2.0.0-rc.15 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /vuepress-shared@2.0.0-rc.52(vuepress@2.0.0-rc.15): - resolution: {integrity: sha512-/Y5Qpl8ueqiggqFAyeJ9U2PhLyE7O5hl3r1k4vER/gKpaeYIqmPxf0ToJMFa3uc4luoNEe/9NJb6LJCrcPjFEA==} - engines: {node: '>=18.19.0', npm: '>=8', pnpm: '>=7', yarn: '>=2'} - peerDependencies: - vuepress: 2.0.0-rc.14 - dependencies: - '@vuepress/helper': 2.0.0-rc.39(vuepress@2.0.0-rc.15) - '@vueuse/core': 10.11.1(vue@3.4.38) - cheerio: 1.0.0-rc.12 - dayjs: 1.11.12 - execa: 9.3.1 - fflate: 0.8.2 - gray-matter: 4.0.3 - semver: 7.6.3 - vue: 3.4.38 - vuepress: 2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38) - transitivePeerDependencies: - - '@vue/composition-api' - - typescript - dev: true - - /vuepress@2.0.0-rc.15(@vuepress/bundler-vite@2.0.0-rc.7)(vue@3.4.38): - resolution: {integrity: sha512-8DRaONwIaG0Jn3P8OKBysTfcO1yPO1UwfUUNQjbToLrbYSsGFiFwpuTMSL3HbdaX7ou6TuJHgK/wuslsUV8jCQ==} - engines: {node: '>=18.16.0'} - hasBin: true - peerDependencies: - '@vuepress/bundler-vite': 2.0.0-rc.15 - '@vuepress/bundler-webpack': 2.0.0-rc.15 - vue: ^3.4.0 - peerDependenciesMeta: - '@vuepress/bundler-vite': - optional: true - '@vuepress/bundler-webpack': - optional: true - dependencies: - '@vuepress/bundler-vite': 2.0.0-rc.7 - '@vuepress/cli': 2.0.0-rc.15 - '@vuepress/client': 2.0.0-rc.15 - '@vuepress/core': 2.0.0-rc.15 - '@vuepress/markdown': 2.0.0-rc.15 - '@vuepress/shared': 2.0.0-rc.15 - '@vuepress/utils': 2.0.0-rc.15 - vue: 3.4.38 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - dependencies: - iconv-lite: 0.6.3 - dev: true - - /whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - dev: true - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: true - - /yaml@2.5.0: - resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} - engines: {node: '>= 14'} - hasBin: true - dev: true - - /yoctocolors@2.1.1: - resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} - engines: {node: '>=18'} - dev: true diff --git a/docs/plans/2026-10-27-telescope-vue3-and-vite-plus.md b/docs/plans/2026-10-27-telescope-vue3-and-vite-plus.md new file mode 100644 index 0000000000..e43f62e0a1 --- /dev/null +++ b/docs/plans/2026-10-27-telescope-vue3-and-vite-plus.md @@ -0,0 +1,343 @@ +# Telescope Vue 3 and shared JavaScript tooling + +## Objective and delivery + +Modernize Telescope's dashboard using Horizon's actual Vue 3 conventions, then consolidate framework JavaScript development under Vite+, pnpm workspaces, and dependency catalogs. Deliver two independently reviewable PRs against `0.4`, in that order. Do not change `0.3` or `main`. + +Work in `/home/binaryfire/workspace/contrib/hypervel/components-vite-plus`. The initial branch is `chore/telescope-vue3`. After PR 1 merges, create PR 2's branch from updated `origin/0.4` in this worktree; do not mix the toolchain migration into PR 1. Commit whole files in focused commits with explanatory bodies. Commit, push, open, or merge PRs only when authorized; never squash by default. + +Read this plan, the monorepo root `CLAUDE.md`, and the worktree's `AGENTS.md` after compaction. This plan does not override their approval, verification, source-reading, or dependency-management requirements. No implementation is authorized merely by writing the plan. + +Part 1 implementation and verification are complete. Remaining work: + +- After Part 1 merges, check the retired lockfile alerts and coordinate Telescope's upstream-sync notes as described below. +- Resolve the formatting approval item before its dependent Part 2 changes. +- After Part 1 merges, implement and verify Part 2. + +## Maintenance tradeoff and approval items + +Both changes address real maintenance costs. Vue 2 constrains Telescope's Vite plugin and retains an unpatched compiler advisory. Retaining Options API, Horizon's patterns, and Telescope's recognizable structure makes future template/lifecycle adaptations straightforward. Request cancellation, routing, and modal behavior still require understanding and tests; upstream patches will not always apply unchanged. A shared toolchain then reduces duplicated dependency decisions, vulnerable lockfile drift, and inconsistent commands across the framework. Use standard Vite+/pnpm behavior first; depart only for a demonstrated requirement. + +The following proposal requires owner approval; writing it here is not approval: + +- **Formatting exception:** permit deterministic Vite+ formatting of maintained first-party frontend source/tests, including ports. Add this approved adaptation to `AGENTS.md`'s Porting Policy during Part 2: “Formatting JavaScript, TypeScript, Vue and frontend CSS sources with the repository's Vite+ formatter configuration, preserving upstream logic, naming, structure and comments.” Use standard root formatter overrides based on upstream conventions; vendored/generated files remain excluded. This gives maintained source normal Vite+ quality coverage without changing its architecture. Owner approval is required before applying the formatter or editing the rule. + +The owner has approved deleting the entire `_archive/docs/0.3` directory. Do this in a separate commit in Part 1, checking remaining documentation/archive references for stale links and preserving other archives. Confirm its lockfile alerts close after GitHub refreshes the graph. This retires inactive documentation and its dependency metadata; it does not fix vulnerable code in a supported application. + +## Shared decisions + +- Keep Laravel-compatible PHP methods, signatures, named arguments, configuration, routes, payloads, authorization, and asset-publishing behavior. Any newly proposed public API departure needs the owner's approval under `AGENTS.md`; this plan assumes none. +- Use Horizon as the concrete frontend reference: Options API objects, component-local state and lifecycle hooks, ordinary Vue Router, explicit imports and component registration, readable methods, and Laravel-style method documentation. Retain Telescope's recognizable filenames, screen boundaries, and feature behavior so upstream changes remain easy to map. No Composition API rewrite, TypeScript conversion of the dashboard, generic UI framework, new state-management library, or permanent Vue compatibility mode. +- Churn is not a reason to retain defects, dead imports, obsolete configuration, or misleading comments. Equally, sharing tooling does not require replacing every library or forcing different supported major versions together. Investigate newly found problems and resolve them under the repository's approval rules. +- No extra request-time PHP work, asset compilation on application startup, network calls for frontend dependencies at runtime, or lazy asset downloads. Preserve polling frequency, cancellation, bounded work, and cleanup. Investigate any meaningful bundle or runtime regression before accepting it; benchmark only after confirming the machine is idle with the owner. +- `components` owns framework frontend development. Maintainers commit source, dependency/lockfile changes, and rebuilt distributable assets together. Subtree splitting copies those files unchanged; Composer consumers do not need Node, pnpm, Vite+, or the root catalog. +- Split repositories retain source but are not standalone frontend development environments after Part 2. Developers customizing them use a fork of `components`, or supply explicit versions and tooling in their own split fork. Do not add catalog-expansion code, synthetic split commits, a second lockfile generator, or a publishing build service. +- Keep application skeletons outside the framework dependency workspace. They retain complete manifests and independent builds after `composer create-project`. Do not create catalog references, parent-directory imports, or framework toolchain dependencies in their shipped files. Their config synchronization and installation workflows remain authoritative. +- No generic dependency updater, custom task scheduler, blanket peer-dependency bypass, broad lint suppression, or new mandatory Git hooks. Use native package-manager, Vite+, Vue, and existing framework facilities. + +## Verified starting points and references + +The paths below are relative to this worktree unless absolute. Read the actual source again before changing it; dependency versions must be checked against current releases and the existing release-age policy when implementation begins. + +| Area | Relevant files and constraints | +| --- | --- | +| Horizon reference | `src/horizon/resources/js/app.js`, `base.js`, `components/Alert.vue`, `components/Poll.vue`, and `screens/recentJobs/index.vue`: Vue 3, Router 4, Options API, native Bootstrap 5 APIs. Its style is a reference, not proof that every existing detail is correct. | +| Telescope | `src/telescope/resources/js/`, `resources/views/layout.blade.php`, `resources/sass/`, `vite.config.js`, `vitest.config.js`, `package.json`, `package-lock.json`: Vue 2.7, Router 3, Bootstrap 4, jQuery, Popper 1, Vue Test Utils 1. | +| Upstream | Read-only Laravel references under `/home/binaryfire/workspace/references/laravel/{telescope,horizon}`. Laravel Telescope currently remains on Vue 2. Compare source/tests and the established upstream tracking records; adapting a dashboard to Vue 3 does not change which upstream fixes need porting. | +| Root tooling | `package.json`, `pnpm-workspace.yaml`, `pnpm-lock.yaml`, `vitest.config.ts`, `tsconfig.json`: pnpm already exists; Horizon, Telescope, and Workbench are excluded. The nested exception renderer and AI workbench are not selected by `src/*`. | +| Existing CI | `.github/workflows/tests.yml` runs root MCP/Wayfinder tests and a separate npm installation/test for Telescope. `.github/workflows/skeletons.yml` builds a fresh base application separately. | +| Distribution | `bin/split.sh` splits committed trees, without rewriting manifests. Horizon/Telescope `dist/`, the renderer's `dist/`, Workbench's `public/build/`, and MCP's minified SDK are committed assets. | + +Primary research: + +- [Vue migration changes](https://v3-migration.vuejs.org/breaking-changes/), [Vue API styles](https://vuejs.org/guide/introduction.html#api-styles), and [Router migration](https://router.vuejs.org/guide/migration/): retain Options API while updating app initialization, router creation, slots, lifecycle hooks, and test render functions. +- [Bootstrap migration](https://getbootstrap.com/docs/5.3/migration/): native component APIs, namespaced data attributes, and changed utilities require corresponding template/Sass updates; changing the dependency alone is insufficient. +- Horizon's actual migration recipes: [Bootstrap/jQuery migration](https://github.com/laravel/horizon/commit/f19486e891310db3eb54ad40520d0af7734a57bd) and [Vue/Router migration](https://github.com/laravel/horizon/commit/267975e4a465625d540974c707a0894400fa15c2). Read their full diffs as discovery, then use current source and supported dependencies rather than copying historical versions. +- [Vite+ monorepos](https://viteplus.dev/guide/monorepo), [migration rules](https://viteplus.dev/guide/migrate-rules), [tests](https://viteplus.dev/guide/test), and [CI](https://viteplus.dev/guide/ci): central quality configuration with package-local build/test configuration, exact toolchain pins, normal workspace task selection, and one setup action. +- [pnpm catalogs](https://pnpm.io/catalogs), [workspace settings](https://pnpm.io/settings), and [lockfile format](https://github.com/pnpm/pnpm/blob/main/pnpm/docs/lockfile.md): catalogs centralize versions, not dependency declarations. Catalog expansion happens during npm packaging, not Git splitting. pnpm 12 may write two YAML documents in one lockfile. +- [Oxc compatibility](https://oxc.rs/compatibility.html): Vue formatting and script linting do not imply Vue template linting. Vue compilation and behavioral tests remain necessary. +- [Dependabot catalogs](https://github.blog/changelog/2025-02-04-dependabot-now-supports-pnpm-workspace-catalogs-ga/), [supported ecosystems](https://docs.github.com/en/code-security/reference/supply-chain-security/supported-ecosystems-and-repositories), and [dependency graph limitations](https://docs.github.com/en/code-security/reference/supply-chain-security/dependency-graph-supported-package-ecosystems): version-update support and security-graph ingestion are separate capabilities. Do not assume a green Dependabot configuration proves transitive advisory coverage. + +## Part 1 — Telescope dashboard migration + +### Buffered-response transport prerequisite + +Remove `Constant::OPTION_BUFFER_OUTPUT_SIZE => 2 * 1024 * 1024` from `src/foundation/config/server.php`. Review this fix together with the complete dashboard migration, then give it a separate commit. This inherited override makes Swoole's process-mode send reject bodies over 2 MiB after HTTP/1.1 headers have been sent, producing empty responses. It affects buffered HTML/JSON and large individual streamed writes. Swoole's native default is `UINT_MAX`; the setting checks send size, not buffer allocation. Use that native default instead of substituting another arbitrary cap. Leave `OPTION_SOCKET_BUFFER_SIZE` unchanged. + +Regenerate both skeleton configs with `composer sync:skeleton-config`, remove the same override from `tests/Benchmarks/Sentry/Fixtures/app/config/server.php`, and run `composer check:skeleton-config`. Check for other maintained copies before committing. Do not edit generated skeleton files directly or change historical archives. No porting-guide or upgrade entry is needed: this removes an internal transport restriction in unreleased 0.4, whose upgrade guidance already requires a fresh application. + +Leave `ResponseBridge` and the dependency unchanged. Native `end()` preserves compression, `Content-Length`, and existing protocol/trailer handling. A PHP `write()` loop would disable Swoole compression and add unnecessary framing and failure paths. Removing the size override does not make buffered responses memory-bounded; streaming and file responses remain the appropriate mechanisms for large generated output. Do not add a compression subsystem, transport abstraction, or new application option. + +Add one focused real process-mode loopback regression using the shipped server settings and `ResponseBridge`: a buffered HTTP/1.1 body over 2 MiB must arrive intact with the correct `Content-Length`. The fixture must consume the production settings so restoring the override makes the test fail; override only fixture isolation settings such as worker count, logging and ports. Follow `ResponseCancellationTest`'s automatic-port, bounded-wait and unconditional-cleanup pattern. Do not add a mock matrix or duplicate compression, trailer and failure-path tests for unchanged bridge code. Direct loopback checks have already verified native compression and a single streamed write over 2 MiB without the override. + +Run the new test file immediately after writing it. At completion run `composer lint:fix`, `composer analyse`, then `composer test:parallel -- --processes=4 tests/HttpServer`, along with the config synchronization check. No separate WebSocket/Reverb/gRPC test expansion is needed for an unchanged bridge. + +### 1. Establish the behavior and build baseline + +Read all Telescope frontend components, screens, mixins, Blade layout, styles, and existing query-preview tests. Compare Horizon's corresponding setup and frontend conventions before editing each area. Use current Laravel Telescope as the behavior reference, including fixes already present in Hypervel. + +Run the current Telescope tests and production build using its existing independent npm setup. Record expected asset filenames and raw/compressed sizes outside the repository. Exercise the existing UI sufficiently to distinguish migration failures from inherited defects. This PR covers Telescope, the server-config correction above, directly required CI/docs updates, and the authorized archive removal. The corresponding Horizon modal correction belongs with its Bootstrap upgrade and shared test setup in Part 2, avoiding an interim independent npm test setup. + +Preserve these contracts: + +- `/telescope` and configured empty, root, or nested Telescope paths; route names, IDs, query strings, history navigation, and deep links. +- Existing API endpoints, request bodies, CSRF behavior, timezone formatting, recording toggles, clear confirmation, and auto-load local-storage preference. +- Lists, filtering, family grouping, pagination, pending-job updates, related entries, dumps, monitored tags, SQL formatting/highlighting, JSON inspection, and clipboard contents. +- Light/dark styling, responsive layout, modal/dropdown behavior, and supported HTTP development installations. +- `Telescope::css()`, `js()`, script variables, nonce handling, and the Blade shell. The dashboard assets are inlined; a build that succeeds but emits unresolved external chunks is not acceptable. + +### 2. Move onto Vue 3 and the Horizon application pattern + +Use package-manager commands to update `src/telescope/package.json` and its npm lockfile. Install supported compatible releases of Vue 3, Router 4, `@vitejs/plugin-vue`, Vue Test Utils 2, and `vue-json-pretty` 2. Replace `@vitejs/plugin-vue2`, Vue 2 compiler dependencies, and the Vue 2 clipboard wrapper. Declare directly imported dependencies; do not rely on npm hoisting to conceal missing declarations. + +Upgrade Telescope's standalone Vite/Vitest to supported versions compatible with these plugins and Node 24. PR 1 still uses plain Vite and npm; Vite+ and workspace/catalog changes belong to Part 2. Preserve the minimum-release-age policy and inspect changes in the complete dependency graph. Do not use `--force`, legacy-peer-deps, or a compatibility shim to pass installation. + +Follow `src/horizon/resources/js/app.js`: + +```js +import { createApp } from 'vue/dist/vue.esm-bundler.js'; +import { createRouter, createWebHistory } from 'vue-router'; + +const app = createApp({ + data() { + return { + // Telescope's existing root-owned state. + }; + }, +}); + +const router = createRouter({ + history: createWebHistory(routerBasePath), + routes: Routes, +}); + +app.use(router); +app.component('preview-screen', PreviewScreen); +app.mixin(Base); +app.mount('#telescope'); +``` + +This is a structural example, not replacement code: retain all state, registrations, listeners, and bootstrap settings. The compiler-enabled Vue import is deliberate because the root template lives in Blade. Do not copy Horizon's product-specific globals or proxy-path configuration into Telescope. Nest each list/detail route pair under a component-less section parent, with `''` and `':id'` children, as Horizon does. Router 4 matches active links by route record, so flat sibling records lose the sidebar highlight on detail pages. Preserve existing names and URLs, and verify active links with the real route definitions. Check initial navigation and route reuse rather than adding speculative waits or forcing remounts. + +Use `defineConfig` and the Vue 3 plugin in Telescope's build and test configs. Rely on the plugin's Options API and production feature-flag defaults; add no redundant `define` settings. For Vite 8 use `build.rolldownOptions` rather than deprecated `rollupOptions`. Retain current output locations and the inline-compatible runtime format; check Lightning CSS output in the visual comparison. No `@vue/compat`, dual Vue runtime, runtime version detection, or Vue 2 translation layer. + +### 3. Convert templates, ownership, and lifecycle behavior + +Keep existing screen/file names and route organization. Match Horizon's component import/registration style and readable Options API method layout. Remove redundant `type="text/ecmascript-6"` script attributes, unused imports/components/data, empty option blocks, and empty styles when no longer needed. Keep useful behavior comments and the intentional upstream exclusion notes. + +- Replace `slot`/`slot-scope` with `#name`/`v-slot`. Convert named slots on real elements into `