Your First Change

4 views Markdown

Change what the platform does without editing a single file it owns. Register one listener, see it take effect, then remove it as cleanly.

Overview

The smallest useful change is a hook listener. It proves the whole loop in a few minutes. Your file is picked up, your code runs at the point you chose, and your return value is honoured. Nothing you did will be overwritten by an upgrade.

This walkthrough blocks a newsletter subscription from throwaway email domains. The rule itself does not matter; what matters is that it is enforced from a file the platform will never replace.

Prerequisites

  • An installation you can break, with the diagnostics on.
  • Write access to the core hooks directory.
  • A page in the theme that submits the form you are going to block, so you can see the refusal.

Walkthrough

Create the Listener File

  1. Create a new file under coremio/hooks named after what it does, for example example-rules.php.
  2. Files in that directory are loaded for you; there is no registry to add yourself to.
  3. Leave it empty for now and reload any page. Nothing should change, which confirms the file is harmless before it does anything.

Register a Listener

  1. Add a registration for the gate that runs before a subscription is stored, with a priority and a closure.
  2. Accept the parameters the hook documents. This one hands you the address and the language.
  3. Return an empty string to allow. At this point the listener runs but changes nothing.

Make It Refuse

  1. Read the domain out of the address and compare it against a small list.
  2. Return a message when it matches. A non-empty return from a gate stops the operation and the message is what the visitor sees.
  3. Submit the form with a matching address; the subscription is refused and your message is displayed. Submit a normal address; it goes through.

Undo It

  1. Delete the file.
  2. Submit again with the address that was refused; it now goes through.
  3. Nothing else has to be reverted, because nothing else was touched. That is the property worth internalising.

Example

coremio/hooks/example-rules.php
Hook::add('gate:client.newsletter_subscribe', 10, function ($email, $lang) {
    $throwaway = ['mailinator.com', 'tempmail.com'];
    $domain    = strtolower((string) substr(strrchr($email, '@') ?: '', 1));

    if (in_array($domain, $throwaway, true))
        return Language::gc('example/permanent-address-required');

    return '';
});

Two lines of that example are worth copying into your own work. The parameters match what the hook documents, and the allow path returns an empty string rather than nothing at all.

the registration contract
// coremio/classes/Hook.php
public static function add($name, $priority, $properties = []): void;
public static function run($name, ...$args): array;
public static function runRefs($name, &...$args): array;

The third argument accepts a closure, as above, or an array naming a class and a method. Use ['class' => 'MyClass', 'method' => 'check'] for an instance call. For a static one, write ['class' => 'MyClass', 'method::static' => 'check']. A lower priority runs earlier, and a collision is nudged rather than dropped.

The other half of the contract is the core side, which decides what your return value means. A gate collects every listener's answer and lets the first non-empty string stop the operation.

coremio/operations/ClientNews.php
foreach (Hook::run('gate:client.newsletter_subscribe', $email, $lang) as $veto)
    if (is_string($veto) && $veto !== '') throw new Exception($veto);

$added = $this->model->add_subscriber($email, $lang);

Returning nothing works by accident; returning an empty string works on purpose. null fails the string test today, and an explicit empty string keeps working if the test ever tightens.

Pitfalls

If nothing happens, suspect your listener before the hook

An error inside a listener is caught and logged rather than shown. A broken listener is then indistinguishable from a hook that never fired. Check the error log first.

A gate runs after the checks in front of it

Validation, rate limiting and bot protection happen before your listener is reached. A request that fails one of those never gets to you. That is the intended order, and it means a gate is the wrong place to test whether the form works at all.

The refusal message is read by a person

Return it through the translation layer rather than as a literal. Otherwise your installation speaks one language to everyone who sees the refusal.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.