-
Notifications
You must be signed in to change notification settings - Fork 0
03 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.
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 system being used correctly:
require_once __DIR__ . '/vendor/autoload.php';
use Affinity4\Magic\Magic;
class User
{
// User model
}
class UserAccount
{
use Magic;
private $User;
public $reputation = 0;
public $level = 0;
public $onReputationChange = [];
public $onLevelUp = [];
public function __construct(\User $User)
{
$this->User = $User;
}
public function setReputation(int $reputation)
{
$current_reputation = $this->reputation;
// We want acces to the user model also in our event listeners
$this->onReputationChange($current_reputation, $reputation, $this->User);
$this->reputation = $reputation;
}
public function getReputation(): int
{
return $this->reputation;
}
public function setLevel(int $level)
{
$current_level = $this->level;
if ($current_level < $level) {
$this->onLevelUp($level, $this->User);
}
$this->level = $level;
}
public function getLevel(): int
{
return $this->level;
}
}
$User = new User;
$UserAccount = new UserAccount($User);
$UserAccount->onReputationChange[] = function(int $current_reputation, int $new_reputation, \User $User) use ($UserAccount)
{
// Chweck this was a reputation increase and by 10 points or more
if ($current_reputation < $new_reputation && $new_reputation >= 10) {
echo "Reputation increased to $new_reputation\n";
// Make sure to use the same instance of $UserAccount
$UserAccount->setLevel(1); // Level up to Level 1
}
};
$UserAccount->onLevelUp[] = function(int $new_level) {
echo "You have leveled up! You're now on Level $new_level!\n";
};
$UserAccount->setReputation(10);
// echos...
// Reputation increased to 10
// You have leveled up! You're now on Level 1!
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:
// ....
// $UserAccount->setReputation(10);
$UserAccount->reputation = 10;
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 comments!
/**
* @property int $reputation
* @property int $level
*/
class UserAccount
{
use Magic;
private $User;
private $reputation = 0; // Change to private
private $level = 0; // Change to private
// ...the rest is uncahnged!
now this...
$UserAccount->reputation = 10;
...will fire our setter events correctly.
Reputation increased to 10
You have leveled up! You're now on Level 1!
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.
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
require_once __DIR__ . '/vendor/autoload.php';
use Affinity4\Magic\Magic;
/**
* @property int $number_of_highlanders
* @property int $lifeforce
*/
class Highlander
{
use Magic;
/**
* @var int
*/
private $number_of_highlanders= 3;
/**
* @var int
*/
private $lifeforce = 10;
public function setNumberOfHighlanders(int $number_of_highlanders)
{
$this->number_of_highlanders= $number_of_highlanders;
}
public function getNumberOfHighlanders(): int
{
return $this->number_of_highlanders;
}
public function setLifeforce(int $lifeforce)
{
$this->lifeforce = $lifeforce;
}
public function getLifeforce()
{
return $this->lifeforce;
}
public function shout(string $phrase)
{
echo $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:
require_once __DIR__ . '/vendor/autoload.php';
use Affinity4\Magic\Magic;
/**
* @property int $lifeforce
* @property int $number_of_highlanders
*/
class Highlander
{
use Magic;
private $lifeforce = 10;
private $number_of_highlanders= 4;
public $onKill = [];
public function setLifeforce(int $lifeforce)
{
$this->lifeforce = $lifeforce;
}
public function getLifeforce(): int
{
return $this->lifeforce;
}
public function setNumberOfHighlanders(int $number_of_highlanders)
{
$this->number_of_highlanders= $number_of_highlanders;
}
public function getNumberOfHighlanders(): int
{
return $this->number_of_highlanders;
}
public function shout(string $phrase)
{
echo $phrase;
}
public function kill(\Highlander $Opponent)
{
$this->onKill($Opponent);
}
}
$Highlander = new Highlander;
$Opponent = new Highlander;
// He killed someone along the way here. But so far only
// he's aware there are only 3 Highlanders left, our player still thinks there are 4
--$Opponent->number_of_highlanders;
$Highlander->onKill[] = function($Opponent) use ($Highlander) {
$Highlander->lifeforce += $Opponent->getLifeforce();
if ($Opponent->number_of_highlanders< $Highlander->number_of_highlanders) {
$Highlander->number_of_highlanders= ($Opponent->number_of_highlanders- 1);
}
echo "You lifeforce is {$Highlander->lifeforce}!\n";
echo "There are {$Highlander->number_of_highlanders} highlanders left\n";
if ($Highlander->number_of_highlanders> 1) {
$Highlander->shout("There can be only one!!!\n");
}
};
$Highlander->kill($Opponent);
// echoes...
// There are only 2 Highlanders left
// You now have 20 lifeforce!
// There can be only one!!
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!