33 |
34 | # Hooks
35 |
36 | ## Installation
37 |
38 | ```shell
39 | composer require glhd/hooks
40 | ```
41 |
42 | ## Usage
43 |
44 | The hooks package provides two types of hooks: hooking into class execution, and hooking into view rendering.
45 |
46 | ### Within Classes
47 |
48 | To make a class "hook-able" you need to use the `Hookable` trait. In your code, you can add `callHook()`
49 | calls anywhere that you want to allow outside code to execute. For example, if you were implementing
50 | a `Session` class, you might want to allow code to hook into before the session starts, and before
51 | the session saves:
52 |
53 | ```php
54 | use Glhd\Hooks\Hookable;
55 |
56 | class MySessionClass implements SessionHandlerInterface
57 | {
58 | use Hookable;
59 |
60 | public function public open(string $path, string $name): bool
61 | {
62 | $this->callHook('beforeOpened', $name);
63 | // ...
64 | }
65 |
66 | public function write(string $id, string $data): bool
67 | {
68 | $this->callHook('beforeWritten');
69 | // ..
70 | }
71 | }
72 | ```
73 |
74 | Now, you can hook into these points from elsewhere in your app:
75 |
76 | ```php
77 | // Get all the available hook points
78 | $hooks = Session::hook();
79 |
80 | // Register your custom code to execute at those points
81 | $hooks->beforeOpened(function($name) {
82 | Log::info("Starting session '$name'");
83 | });
84 |
85 | $hooks->beforeWritten(function() {
86 | Log::info('Writing session to storage');
87 | });
88 | ```
89 |
90 | Now, whenever `MySessionClass::open` is called, a `"Starting session ''"` message will be logged,
91 | and whenever `MySessionClass::write` is called, a `"Writing session to storage"` message will be logged.
92 |
93 | ### Hook Priority
94 |
95 | You can pass an additional `int` priority to your hooks, to account for multiple hooks
96 | attached to the same point. For example:
97 |
98 | ```php
99 | $hooks->beforeOpened(fn($name) => Log::info('Registered First'), 500);
100 | $hooks->beforeOpened(fn($name) => Log::info('Registered Second'), 100);
101 | ```
102 |
103 | Would cause "Registered Second" to log before "Registered First". If you don't pass a priority, the
104 | default of `1000` will be used. All hooks at the same priority will be executed in the order they
105 | were registered.
106 |
107 | ### Stopping Propagation
108 |
109 | Hooks can halt further hooks from running with a special `stopPropagation` call (just like JavaScript).
110 | All hooks receive a `Context` object as the last argument. Calling `stopPropagation` on this object
111 | will halt any future hooks from running:
112 |
113 | ```php
114 | use Glhd\Hooks\Context;
115 |
116 | $hooks->beforeOpened(function($name) {
117 | Log::info('Lower-priority hook');
118 | }, 500);
119 |
120 | $hooks->beforeOpened(function($name, Context $context) {
121 | Log::info('Higher-priority hook');
122 | $context->stopPropagation();
123 | }, 100);
124 | ```
125 |
126 | In the above case, the `'Lower-priority hook'` message will never be logged, because a higher-priority
127 | hook stopped propagation before it could run.
128 |
129 | ### Passing data between your code and hooks
130 |
131 | There are three different ways that data gets passed in and out of hooks:
132 |
133 | 1. Passing arguments *into* hooks (one-way)
134 | 2. Returning values *from* hooks (one-way)
135 | 3. Passing data into hooks that can be mutated by hooks (two-way)
136 |
137 | #### One-way data
138 |
139 | Options 1 and 2 are relatively simple. Any positional argument that you pass to `callHook` will
140 | be forwarded to the hook as-is. In our example above, the `beforeOpened` call passed `$name` to
141 | its hooks, and our hook accepted `$name` as its first argument.
142 |
143 | A collection of returned values from our hooks is available to the calling code. For example,
144 | if we wanted to allow hooks to add extra recipients to all email sent by our `Mailer` class,
145 | we might do something like:
146 |
147 | ```php
148 | use Glhd\Hooks\Hookable;
149 |
150 | class Mailer
151 | {
152 | use Hookable;
153 |
154 | protected function setRecipients() {
155 | $recipients = $this->callHook('preparingRecipients')
156 | ->filter()
157 | ->append($this->to);
158 |
159 | $this->service->setTo($recipients);
160 | }
161 | }
162 | ```
163 |
164 | ```php
165 | // Always add QA to recipient list in staging
166 | if (App::environment('staging')) {
167 | Mailer::hook()->preparingRecipients(fn() => 'qa@myapp.com');
168 | }
169 | ```
170 |
171 | It's important to note that you will **always** get a collection of results, though, even
172 | if there is only one hook attached to a call, because you never know how many hooks may
173 | be registered.
174 |
175 | #### Two-way data
176 |
177 | Sometimes you need your calling code and hooks to pass the same data in two directions. A
178 | common use-case for this is when you want your hooks to have the option to abort execution,
179 | or change some default behavior. You can do this by passing named arguments to the call,
180 | which will be added to the `Context` object that is passed as the last argument to your hook.
181 |
182 | For example, what if we want hooks to have the ability to *prevent* mail from sending at all?
183 | We might do that with something like:
184 |
185 | ```php
186 | use Glhd\Hooks\Hookable;
187 |
188 | class Mailer
189 | {
190 | use Hookable;
191 |
192 | protected function send() {
193 | $result = $this->callHook('beforeSend', $this->message, shouldSend: true);
194 |
195 | if ($result->shouldSend) {
196 | $this->service->send();
197 | }
198 | }
199 | }
200 | ```
201 |
202 | ```php
203 | // Never send mail to mailinator addresses
204 | Mailer::hook()->beforeSend(function($message, $context) {
205 | if (str_contains($message->to, '@mailinator.com')) {
206 | $context->shouldSend = false;
207 | }
208 | });
209 | ```
210 |
211 | ### When to use class hooks
212 |
213 | Class hooks are mostly useful for package code that needs to be extensible without
214 | knowing **how** it will exactly be extended. The Laravel framework provides similar extension
215 | points, like [`Queue::createPayloadUsing`](https://github.com/laravel/framework/blob/443ec4438c48923c9caa9c2b409a12b84a10033f/src/Illuminate/Queue/Queue.php#L288).
216 |
217 | In general, you should avoid using class hooks in your application code unless you are
218 | dealing with particularly complex conditional logic that really warrants this approach.
219 |
220 | ## Within Views
221 |
222 | Sometimes you may want to make certain views "hook-able" as well. For example, suppose
223 | you have an ecommerce website that sends out email receipts, and you want to occasionally
224 | add promotions or other contextual content to the email message. Rather than constantly
225 | adding and removing a bunch of `@if` calls, you can use a hook:
226 |
227 | ```blade
228 | {{-- emails/receipt.blade.php --}}
229 | Thank you for shopping at…
230 |
231 |
232 |
233 | Your receipt info…
234 |
235 |
236 | ```
237 |
238 | Now you have two spots that you can hook into…
239 |
240 | ```php
241 | // Somewhere in a `PromotionsServiceProvider` class, perhaps…
242 |
243 | if ($this->isInCyberMondayPromotionalPeriod()) {
244 | View::hook('emails.receipt', 'intro', fn() => view('emails.promotions._cyber_monday_intro'));
245 | }
246 |
247 | if (Auth::user()->isNewRegistrant()) {
248 | View::hook('emails.receipt', 'footer', fn() => view('emails.promotions._thank_you_for_first_purchase'));
249 | }
250 | ```
251 |
252 | The `View::hook` method accepts 4 arguments. The first is the view name that you're
253 | hooking into; the second is the name of the hook itself. The third argument can either
254 | be a view (or anything that implements the `Htmlable` contract), or a closure that returns
255 | anything that Blade can render. Finally, the fourth argument is a `priority` value—the lower
256 | the priority, the earlier it will be rendered (if there are multiple things hooking into
257 | the same spot). If you do not provide a priority, it will be set the `1000` by default.
258 |
259 | ### Explicitly Setting View Name
260 |
261 | The `` Blade component can usually infer what view it's being rendered inside.
262 | Depending on how your views are rendered, though, you may need to explicitly pass the view
263 | name to the component. You can do that by passing an additional `view` prop:
264 |
265 | ```blade
266 |
267 | ```
268 |
269 | This is a requirement that we hope to improve in a future release!
270 |
271 | ### View Hook Attributes
272 |
273 | It's possible to pass component attributes to your hooks, using regular Blade syntax:
274 |
275 | ```blade
276 |
277 | ```
278 |
279 | Your hooks will then receive the `status` value (and any other attributes you pass):
280 |
281 | ```php
282 | View::hook('my.view', 'status', function($attributes) {
283 | assert($attributes['status'] === 'Demoing hooks');
284 | });
285 | ```
286 |
287 | If you pass the hook a Laravel view, any attributes will automatically be forwarded.
288 | This means that you can use the `$status` variable inside your view. For example,
289 | given the following views:
290 |
291 | ```blade
292 | {{-- my/view.blade.php --}}
293 |
294 |
295 | {{-- my/hook.blade.php --}}
296 |
297 | Your current status is '{{ $status }}'
298 |