[Translation] Magic methods in PHP

If you have ever studied the PHP code of open source projects, then you may come across methods that start with double underscores. These are the very magical methods with which you can determine the behavior of your object during various manipulations with its instance.

I assume that you have already come across some of them, because there are quite common methods, and nevertheless, I believe that a competent PHP programmer needs to be confident in all the features of the language.
I think this can be considered a kind of starting point in the world of Magical methods.

Getting started


When I studied this material myself, I used all kinds of textbooks and articles that set out rather stupid or generally useless examples. I believe that in order to understand something you need to try this in the context of a real task. This is where we will begin.

Imagine that we want to get all the tweets using Tweeter Api. We get the JSON of all the tweets of the current user and we want to turn each tweet into an object with methods that will allow certain operations.

Below, I introduced the base Tweet class:
class Tweet {
}


Now that we have created the object, we can begin to study the methods themselves. (Note translator - some designs will sometimes be omitted to emphasize the role and capabilities of each method)

Constructors and Destructors


Perhaps one of the most common magic methods is the constructor (__construct ()). If you followed the Cribbb application on my blog closely enough, you are quite aware of this method.

The __construct () method is automatically called when an object is instantiated. In it you can set the initial properties of the object or set the dependencies.

Usage example:

public function __construct($id, $text)
{
  $this->id = $id;
  $this->text = $text;
}
$tweet = new Tweet(123, 'Hello world');


When we instantiate the Tweet class, we can pass the parameters that go into the __construct () method. From the example above, you can see that we do not call this method and should not call it - it is called automatically.

Over time, you will need to extend the class by inheriting it. Sometimes the parent class also has the __construct () method, which performs certain actions, so in order not to lose the functionality of the parent class, you need to call its constructor.

class Entity {
  protected $meta;
  public function __construct(array $meta)
  {
    $this->meta = $meta;
  }
}
class Tweet extends Entity {
  protected $id;
  protected $text;
  public function __construct($id, $text, array $meta)
  {
    $this->id = $id;
    $this->text = $text;
    parent::__construct($meta);
  }
}


When you try to delete an object, the __destruct () method will be called. Again, by analogy with the constructor, this is not something that needs to be called, because PHP will do everything for you. This method will allow you to clear everything that you used in the object, for example, a database connection.

public function __destruct()
{
  $this->connection->destroy();
}


To be honest, I have hidden most of the __destruct () method described above from you. PHP is actually not one of those languages ​​where the process will exist for a long enough time, so I don’t think that you will have anything for which a destructor might be needed. The request life cycle in PHP itself is so small that this method will be more trouble than good.

Getters and Setters


When you work with objects in PHP, you would really like to refer to the properties of the object like this:

$tweet = new Tweet(123, 'hello world');
echo $tweet->text; // 'hello world'


However, if the protected property modifier is set on the text property, then such a call will cause an error.
The __get () magic method will catch calls to any non-public properties.

public function __get($property)
{
  if (property_exists($this, $property)) {
    return $this->$property;
  }
}


The __get () method takes the name of the property you are accessing as an argument. In the above example, the existence of a property in the object is first checked, and if it exists, then its value is returned.

As in the examples above - you should not call this method directly, PHP will call it every time you try to access non-public class properties.

In the opposite situation - if you try to set the value of a property that is not public - you will get an error. And again, in PHP there is a method that will be called when trying to set a value to a non-public field. This method takes 2 parameters as arguments - the property in which they wanted to write the value, and the value itself.

If you want to use this method, your class will get a property, similar to this:

public function __set($property, $value)
{
  if (property_exists($this, $property)) {
    $this->$property = $value;
  }
}
$tweet->text = 'Setting up my twttr';
echo $tweet->text; // 'Setting up my twttr'


In the above examples, I showed how you can get or set the values ​​of properties that do not have a public access modifier. However, working with these magic methods will not always be the best idea. It is much better to have many methods for receiving and writing properties, since in this case they form a specific API and this means that when you change the storage or processing method, your code will not be broken.

However, you will still sometimes encounter the __get () and __set () methods, which are commonly called getters and setters, respectively. This is a pretty good solution if you decide to change a value or add a bit of business logic.

Validating a property for existence


If you are familiar with PHP, you are most likely aware of the existence of the isset () function, which is usually used when working with arrays. You can also use this function in order to understand whether a property is set in an object or not. You can define the magic __isset () method so that you can check not only public properties, but also others.

public function  __isset($property)
{
  return isset($this->$property);
}
isset($tweet->text); // true


As you see above, the __isset () method monitors the function call for existence checking and receives, as an argument, the name of the property. In turn, in the method you can use the isset () function to check for existence.

Clearing a variable
By analogy with the isset () function, the unset () function is usually used when working with arrays. Again, you can use the unset () function to clear the value of a non-public property. To apply this method to non-public properties, you need the __unset () method, which will track attempts to clear non-public class properties.

public function __unset($property)
{
  unset($this->$property);
}


Cast to string


The __toString () method will allow you to determine the logic of your application when trying to cast an object to a string type.
For instance:

public function __toString()
{
  return $this->text;
}
$tweet = new Tweet(1, 'hello world');
echo $tweet; // 'hello world'


We can say that when you try to refer to an object as a string, for example, when using echo, the object will be returned as you define in the __toString () method.

A good illustration in this case is the Eloquent Models from the Laravel framework. When you try to cast an object to a string, you get json. If you want to see how Laravel does this, I recommend referring to the source code .

Sleep and Awakening


The serialization function (serialize ()) is a fairly common way to store an object. For example, if you wanted to save an object in a database, you would first have to serialize it, then save it, and when you need it again, you would have to get it and deserialize it (unserialise ()).

The __sleep () method allows you to determine which properties should be saved. If, for example, we did not want to preserve any connections or external resources.

Imagine that when we create an object, we want to determine the mechanism for its conservation.

$tweet = new Tweet(123, 'Hello world', new PDO ('mysql:host=localhost;dbname=twttr', 'root'));


When we prepare to save the object, we naturally do not need to save the connection to the database, because in the future it will be pointless.
Therefore, in the __sleep () method, we define an array of properties that should be saved.

public function __sleep()
{
  return array('id', 'text');
}


And after the time comes for awakening the object, we may need everything that we did not save during serialization. In a specific example, we need to establish a connection to the database. This can be done using the magic __wakeup () method.

public function __wakeup()
{
  $this->storage->connect();
}


Method call


The __call () magic method will intercept all attempts to call non-public methods. For example, you may have an array of data that you want to modify:

class Tweet {
  protected $id;
  protected $text;
  protected $meta;
  public function __construct($id, $text, array $meta)
  {
    $this->id = $id;
    $this->text = $text;
    $this->meta = $meta;
  }
  protected function retweet()
  {
    $this->meta['retweets']++;
  }
  protected function favourite()
  {
    $this->meta['favourites']++;
  }
  public function __get($property)
  {
    var_dump($this->$property);
  }
  public function __call($method, $parameters)
  {
    if (in_array($method, array('retweet', 'favourite')))
    {
      return call_user_func_array(array($this, $method), $parameters);
    }
  }
}
$tweet = new Tweet(123, 'hello world', array('retweets' => 23, 'favourites' => 17));
$tweet->retweet();
$tweet->meta; // array(2) { ["retweets"]=> int(24) ["favourites"]=> int(17) }


Another typical example is the use of another public API in your object.

class Location {
	protected $latitude;
	protected $longitude;
	public function __construct($latitude, $longitude)
	{
		$this->latitude = $latitude;
		$this->longitude = $longitude;
	}
	public function getLocation()
	{
		return array(
			'latitude' => $this->latitude,
			'longitude' => $this->longitude,
		);
	}
}
class Tweet {
	protected $id;
	protected $text;
	protected $location;
	public function __construct($id, $text, Location $location)
	{
		$this->id = $id;
		$this->text = $text;
		$this->location = $location;
	}
	public function  __call($method, $parameters)
	{
		if(method_exists($this->location, $method))
		{
		  return call_user_func_array(array($this->location, $method), $parameters);
		}
	}
}
$location = new Location('37.7821120598956', '-122.400612831116');
$tweet = new Tweet(123, 'Hello world', $location);
var_dump($tweet->getLocation()); // array(2) { ["latitude"]=> string(16) "37.7821120598956" ["longitude"]=> string(17) "-122.400612831116" }


In the above example, we can call the getLocation method on an object of the Tweet class, but in fact we delegate it to the Location class.
If you are trying to call a static method, you can also use the __callStatic () magic method. The main thing to remember is that it only works when calling static methods.

Cloning


When you create a copy of an object in PHP, in fact, not a new object is written into the variable, but an identifier that refers to the original object. That is, any change in the referencing object entails a change in the original object, however, deleting any of the objects will not affect the existence of others:

$sheep1 = new stdClass;
$sheep2 = $sheep1;
$sheep2->name = "Polly";
$sheep1->name = "Dolly";
echo $sheep1->name; // Dolly
echo $sheep2->name; // Dolly


$a = new StdClass;
$b = $a;
$a = null;
var_dump($b); // object(stdClass)#1 (0) {  }


To create a copy of an object, you should use the clone keyword.

$sheep1 = new stdClass;
$sheep2 = clone $sheep1;
$sheep2->name = "Polly";
$sheep1->name = "Dolly";
echo $sheep1->name; // Dolly
echo $sheep2->name; // Polly


However, if we have several related objects, the dependencies that are in them will also be copied.

class Notification {
  protected $read = false;
  public function markAsRead()
  {
    $this->read = true;
  }
  public function isRead()
  {
    return $this->read == true;
  }
}
class Tweet {
  protected $id;
  protected $text;
  protected $notification;
  public function __construct($id, $text, Notification $notification)
  {
    $this->id = $id;
    $this->text = $text;
    $this->notification = $notification;
  }
  public function  __call($method, $parameters)
  {
    if(method_exists($this->notification, $method))
    {
      return call_user_func_array(array($this->notification, $method), $parameters);
    }
  }
}
$tweet1 = new Tweet(123, 'Hello world', new Notification);
$tweet2 = clone $tweet1;
$tweet1->markAsRead();
var_dump($tweet1->isRead()); // true
var_dump($tweet2->isRead()); // true


In order to solve this problem, we can define the __clone () method in order to determine the correct behavior:

class Tweet {
  protected $id;
  protected $text;
  protected $notification;
  public function __construct($id, $text, Notification $notification)
  {
    $this->id = $id;
    $this->text = $text;
    $this->notification = $notification;
  }
  public function  __call($method, $parameters)
  {
    if(method_exists($this->notification, $method))
    {
      return call_user_func_array(array($this->notification, $method), $parameters);
    }
  }
  public function  __clone()
  {
    $this->notification = clone $this->notification;
  }
}
$tweet1 = new Tweet(123, 'Hello world', new Notification);
$tweet2 = clone $tweet1;
$tweet1->markAsRead();
var_dump($tweet1->isRead()); // true
var_dump($tweet2->isRead()); // false


Calling an object as a function


The magic method __invoke () allows you to determine the logic of the object, when you try to access the object as a function.

class User {
  protected $name;
  protected $timeline = array();
  public function __construct($name)
  {
    $this->name = $name;
  }
  public function addTweet(Tweet $tweet)
  {
    $this->timeline[] = $tweet;
  }
}
class Tweet {
  protected $id;
  protected $text;
  protected $read;
  public function __construct($id, $text)
  {
    $this->id = $id;
    $this->text = $text;
    $this->read = false;
  }
  public function __invoke($user)
  {
    $user->addTweet($this);
    return $user;
  }
}
$users = array(new User('Ev'), new User('Jack'), new User('Biz'));
$tweet = new Tweet(123, 'Hello world');
$users = array_map($tweet, $users);
var_dump($users);


In this example, I am applying the $ tweet object as a callback function to all the values ​​of the $ users array. In this example, we will add a tweet to each user. I agree, this example is a bit artificial, but I'm sure that you really will find application for this method.

Conclusion


As you can see, PHP uses magic methods to respond to certain actions with methods or properties of an object. Each of these methods works automatically, you simply determine what should happen, and PHP takes care of the rest.

For quite some time I did not understand the true meaning of magical methods. I thought they were needed only to do some interesting things with objects. And when I finally understood their true purpose, I was able to write more powerful objects in the framework of more serious applications.

I hope that in each of the examples presented in this guide, I was able to show how magic methods can help in everyday tasks. And I will not be the first to agree that when you explain some material, but do not explain its practical application, it is really annoying.

Author: Philip Brown
Original: culttt.com/2014/04/16/php-magic-methods
Acknowledgments: werdender , HighQuality

PS Sorry, I know that the translation in some places is pretty clumsy. If you know how it would sound better - write to me, I will try to fix it.

Also popular now: