Download the PHP package affinity4/magic without Composer
On this page you can find all versions of the php package affinity4/magic. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download affinity4/magic
More information about affinity4/magic
Files in affinity4/magic
Package magic
Short Description Magic Trait used to easily add event listeners, spelling suggestions in errors and Javascript __set and __get style setters an getters to any class. Magic!
License MIT
Informations about the package magic
Affinity4 Magic
Magic Trait used to easily add event listeners, spelling suggestions in errors and Javascript set and get style setters an getters to any class. Magic!
See the Wiki for this repo for full documentation
Installation
Event Listeners
Simply include Magic in any class to instantly have event listeners!
Once you've included Magic as a trait you can then add any public "camelCased" property starting with "on". You now have an event listener! That's all it takes!
Let's say we have a Model called User
When a new user is registered, we want to email them to let them know their login details.
We'll add the Magic trait and create a public onRegistration
property. It must be an array.
Now each time User::register()
is called the User::onRegistration()
method will also be called, with the users details available to any event listener attached.
Event Listeners
To attach an event listener you simply need to add a callback to the onRegistration array. They will then be called in order every time User::registration()
is executed.
Of course you'll want to do something more clever (and security conscious) than this but you get the idea.
"Chained" or "nested" events
IMPORTANT
One thing to always be conscious of is that event listeners are not shared across all instances of the class. If you create the following:
No log event will be fired. This is because the events listener that will log the email is only listening to $EmailA
.
This might be fairly obvious when side-by-side like this but in a large project this can be confusing if you forget what instance you are dealing with and what events are bound to it. You could get your logs mixed up, or worse. SO BE CAREFUL!
Containers for Scalability
This is where ServiceManagers, or IoC and DI Containers, are a life saver. However, because Containers will by default always return the same instance of the class when you get it from the container, you will need to use factories if you intend to set your events in the container while creating the class.
However, sometimes it's very powerful to have events unique to each instance. For games with multiple instances of a "Player" class, you don't want every player getting points for a kill do you?
You'll see an example of this in the "Magic Setters and Getters" section
Magic Properties
Another enhancement the Magic trait gives you is the ability to ensure setter and getter methods are called every time you set or get a value directly from a property outside of it's defining class, whether you use the setter/getter methods or not.
Consider this academic example of a user account on a platform like StackOverflow. You have an account with reputation points. There is an event to be called once the user gets to the next "level" and gains access to new features, and so other events can be fired, like emailing them or moderators etc.
IMPORTANT: There is a big mistake here! The mistake here is that the $reputation
property has been set to public, allowing the events to be bypassed by mistake.
Let's take a look at an example of this mistake.
Note the lack of @property
docblock attributes on the UserAccount
class, and that $reputation
and $level
are both public:
NOTE: You can set it to 9 to verify the level up event doesn't happen if you want.
This is all well and good while things are used as expected, however, because the reputation property and the level property are left as public, the following can be done:
Nothing happens. You could even directly set the level property and nothing would happen. The system is unaware these properties changed.
Magic can fix this just by changing the properties to protected or private and adding 2 doc block attributes!
now this...
...will fire our setter events correctly.
You can still use you're setters and getters as normal of course! But if you forget to, Magic will happen and keep your system working as expected.
Highlander game example
To show how all this can save you tons of conditional if/else/elseif code that becomes a nightmare to maintain, check out this game (or the start of one at least), based on 1986 movie The Highlander. You know, "There can be only one" and all that.
Requirements:
- There must be a
Highlander
class that all players are an instance of - Each player starts the game with a "lifeforce" (not health related) of 10
- When a player kills another player they absorb/gain that opponents lifeforce, whatever it may be at the time
- We will be aware of how many highlanders are left only when we've killed another player
- If there are still other players to defeat the player will shout "There can be only one!"
That's basically the plot of the movie :)
So, first we create a class called Highlander that uses Affinity4\Magic\Magic
with 2 private properties $number_of_highlanders
and $lifeforce
. These will have setter/getter methods set/get_number_of_highlanders
and set/getLifeforce
. We'll add @property
docblock attributes for $number_of_highlanders
and $lifeforce
to enable the magic. We'll also have a shout
method that just echoes a phrase
Next, we create the kills
method, which takes in the instance of the player you killed (so you can take their lifeforce etc). It fires the onKill
event with the defeated player passed in:
Not only is this less than 75 lines, but no method in the Highlander class has more than 1 line of code! And it will never need to. From now on if we decide we need more to happen when someone gets killed or makes a kill we just add more event handlers!
If that's not magic I don't what is!
Invokable Classes as Event Handlers
While callbacks as event handlers are convenient and quick to write, they have limitations and can often encourage bad design choices.
For example, our Highlander game example in the Magic Properties page, which used callbacks as event handler only had about 11 lines of code. However, it already has serious problems that will only get worse as more lines are added or more callbacks are added.
This is the event handler:
Problem 1: Enforcing Types
Let's start with the first line:
The issue here is that we cannot enforce types. We can type hint $Opponent
in our Highlander::kill()
method that fires the onKill
method, but that assumes we're passing the same value through to the kill
method. We may in fact be passing it a generated value, that could be anything.
We're also unable to ensure $Highlander
is actually an instance of \Highlander
. If something else gets passed in we'll either get errors or worse, we could pass in another class with the same properties and methods that does completely unexpected things. This would mean no errors, but quite possibly hard to debug side-effects.
Problem 2: Single Responsibility
With only a callback to add our code to, we lose the organisational benefits of OOP. It's quite easy to end up breaking SRP without even realizing, especially on projects with numerous developers.
While out code looks initially like it all belongs together, with some closer examination, we can see it's actually modifying 2 parts of our Highlander class, updating $lifeforce
and updating $number_of_highlanders
The first 2 lines are only dealing with the $lifeforce
property, and should be moved out of this function. However, splitting everything out into their own callback would quickly become messy and hard to maintain. Callbacks and closures would require reading the code to determine what they are doing. If these lines were instead refactored to a class we would know what each class is for and what each method should be doing from the names (which should be clear and descriptive). We would also have everything else classes provide which callbacks do not.
Problem 3: Organization
How should we organize all of this? Should we simply create a separate file for each event in the application and dump everything in each file? We could, but I can imagine that becoming pretty horrible after a while.
Instead, if we had autoloading and a sensible folder structure we could simply loop over autoloaded classes and add event listeners to events. This would mean creating a new class in the right folder would be all it would take to bind a handler to an event.
Solution
Invokable classes can solve all of these problems, and give a few more perks that only OOP can provide. So let's refactor our existing code in to 2 separate event handler classes LifeforceEventHandler
and NumberOfHighlandersEventHandler
.
TakeOpponentsLifeforceEventHandler
The only requirement of an invokable event handler class is the it has an __invoke()
method with the same arguments as out callback. However, we can now do more "setup" using the constructor as well.
Our event handler would now look something like:
We can now now enforce our $Highlander
and $Opponent
arguments are \Highlander
instances. Really we should be using an interface here but that's up to you.
It's also quite clear that this classes purpose is to deal with anything to do with taking your opponents lifeforce.
We could even use the Magic trait here and fire an event for other classes to subscribe to. Let's say we need to add a SpecialAbility
feature that gives a player a random special ability after they hit 50 lifeforce points. We could simply add an event onFiftyLifeforce
in out __invoke()
method. Now our special ability class could subscribe to this event to do what it needs to do.
UpdateNumberOfHighlandersEventHandler
It should be pretty obvious how to implement the UpdateNumberOfHighlandersEventHandler
but for completeness sake let's see it
Calling the Event Handlers
To attached our event handlers we simply replace the callbacks with the initialized EventHandler class, like so:
Internally, the invoke methods will be used and passed in our the $Opponent
instance from the kill()
method
TODO
- Use PHP 8 Attributes instead of magic set/get
- Improve examples to use PSR compliant container examples