_construct()
or
__get()
. The magic methods in PHP are loopholes that help developers do amazing things. The network is full of instructions for using them, with which you are probably familiar. But what if we say that you have not even seen the real magic? After all, the more it seems to you that you know everything, the more magic eludes you.
Let's drop the established framework of OOP rules and make the impossible possible at the PHP school of magic. The main and first magical teacher of the school is Alexander Lisachenko ( NightTiger ). He will teach magic thinking and perhaps you will love magic methods, non-standard ways of accessing properties, changing contexts, aspect-oriented programming and stream filters.
Alexander Lisachenko is the head of the department of web development and architecture in Alpari. The author and lead developer of the aspect-oriented framework Go! Aop . Speaker at international conferences on PHP.
In the good movie "The Illusion of Deception" there is a phrase:
"The closer you are, the less you see."
The same can be said about PHP, as a magic trick that allows you to do unusual things. But first of all, it was created to deceive you: "... an action that is intended to deceive, either as a way of cheating someone, or as a joke or form of entertainment."
If we take PHP together and try to write something magical on it, most likely I will deceive you. I will do some trick, and you will be wondering for a long time why this happens. That's because PHP is a programming language known for its unusual stuff.
Magic equipment
What do we need from magic equipment? Painfully familiar methods.
__construct(), __destruct(), __clone(),
__call(), __callStatic(),
__get(), __set(), __isset(), __unset(),
__sleep(), __wakeup(),
__toString(), __invoke(), __set_state(),
__debugInfo()
I will note the last method separately - with it you can crank up unusual things. But that's not all.
-
declare(ticks=1)
.
-
debug_backtrace()
. This is our companion to understand where we are in the current code, to see who called us and why, with what arguments. Useful to decide not to follow all the logic.
-
unset(), isset()
. It seems that nothing special, but these designs hide a lot of tricks, which we will consider further.
-
by-reference passing
. When we pass some object or variable by reference, we should expect that some magic will inevitably happen to you.
-
bound closures
. On the closures and the fact that they can bind, you can build a lot of tricks.
- The Reflection API helps take reflection to a new level.
- StreamWrapper API.
The equipment is ready - I recall the first rule of magic.
Always be the smartest guy in the room.
Trick # 1. Impossible comparison
Let's start with the first trick, which I call Impossible Comparison.
Take a good look at the code and see if this can happen in PHP.
There is a variable, we declare its value, and then suddenly it is not equal to itself.
Not-a-number
There is such a magical artifact as NaN - Not-a-number.
Its amazing feature is that it is not equal to itself. And this is our first trick: use NaN to puzzle a friend. But NaN is not the only solution for this task.
We use constants
The trick is that we can declare
false
as
true
for
namespace
and compare. The unlucky developer will wonder for a long time why there is
true
and not
false
.
Handler
The next trick is more powerful artillery than the two previous options. Let's see how it works.
The trick is based on
tick_function
and, as I mentioned,
declare(ticks=1)
.
How does it all work out of the box? First, we declare some function, and by reference it takes the
isMagic
parameter, and then it tries to change this value to
true
. After we declared
declare(ticks=1)
, the PHP interpreter calls
register_tick_function
- callback after each elementary operation. In this callback, we can change the value that was previously
false
to
true
. Magic!
Trick # 2. Magic expressions
Take an example in which two variables are declared. One of them is
false
, the other is
true
. We
isBlack
and
isWhite
and var_dump the result. What do you think will be the result?
Priority operators . The correct answer is
false
, because in PHP there is such a thing as โoperator precedenceโ.
Surprisingly, the logical operator
or
less priority than the assignment operation. Therefore, simply assigning
false
occurs. In
isWhite
can be any other expression that will execute if the first part fails.
Magic expressions
Take a look at the code below. There is some class that contains the constructor, and some factory, the code of which will be further.
Pay attention to the last line.
$value = new $factory->build(Foo::class);
There are several options that can happen.
-
$factory
will be used as the class namenew
;
- there will be a parsing error;
- the call
$factory->build()
will be used, the value of which this function will return, the result will benew
;
- the value of the
$factory->build
property will be used to construct the class.
Let's check out the last idea. In the
$factory
class, we declare a series of magic functions. They will write what we do: call the property, call the method, or even try to call
invoke
on the object.
class Factory { public function _get($name) {echo "Getting property {$name}"; return Foo::class;} public function _call($name, $arg) {echo "Calling method {$name}"; return Foo::class;} public function _invoke($name) {echo "Invoking {$name}"; return Foo::class;} }
The correct answer: we are not calling a method, but a property. After
$factory->build
there is a parameter for the constructor, which we will pass to this class.
By the way, in this framework I have implemented a feature called "intercepting the creation of new objects" - you can "lock" this design.
Parser loophole
The following example is for the PHP parser itself. Have you ever tried calling functions or assigning variables inside curly braces?
I got this trick on Twitter, it works extremely non-standard.
$result = ${'_' . !$_=getCallback()}(); $_=getCallback(); // string(5) "hello" !$_=getCallback()}(); // bool(false) '_'.!$_=getCallback()}(); // string(1) "_" ${'_'.!$_=getCallback()}(); // string(5) "hello"
First, we assign the value of the expression to the variable named
_
(underscore). We already have a variable, we try to logically invert its value, and we get
false
- the line is casted as if to
true
. Then we glue it all together in the variable name, through which we then turn inside the curly brackets.
What is magic? This entertainment, which allows us to feel inspired, unusual, say: โWhat? So it was possible ?! โ
Trick # 3. Breaking the rules
What I like about PHP is that you can break the rules that everyone creates while trying to be super secure. There is a design called a "sealed class" that has a private constructor. Your task as a student of the magician is to create an instance of this class.
Consider three options for how to do this.
Workaround
The first way is the most obvious. It should be familiar to every developer - this is the standard API that the language offers us.
The
newInstanceWithoutConstructor
construct allows you to bypass the language restrictions that the constructor is private and create an instance of the class bypassing all of our private constructor declarations.
The option is working, simple, does not require any explanation.
Short circuit
The second option requires more attention and skill. An anonymous function is created, which then binds to the osprey of that class.
Here we are inside the class and can safely call private methods. We use this by calling
new static
from the context of our class.
Deserialization
The third option is the most advanced, in my opinion.
If you write a specific line in a specific format, substitute certain values โโthere - our class will turn out.
After deserialization, we get our
instance
.
doctrine / instantiator package
Magic often becomes a documented framework or library - for example, in doctrine / instantiator all this is implemented. We can create any objects with any code.
composer show doctrine/instantiator --all name : doctrine/instantiator descrip. : A small, lightweight utility to instantiate objects in PHP without invoking their constructors keywords : constructor, instantiate type : library license : MIT License (MIT)
Trick # 4. Intercepting property access
The clouds are gathering: the class is secret, the properties and constructor are private, and also callback.
class Secret { private $secret = 42; private function _construct() { echo 'Secret is: ', $this->secret; } private function onPropAccess(string $name) { echo "Accessing property {$name}"; return 100500; } } // How to create a secret instance and intercept the secret value?
Our task, as wizards, is somehow to call a callback.
Adding the magic ... getter
Add a pinch of magic to make it work.
This pinch of magic is a magical
getter
. It calls a function, and so far nothing terrible has happened. But we will use the previous trick and create an instance of this object bypassing the private construct.
Now we need to somehow call callback.
"Unset" inside the circuit
To do this, create a closure. Inside the closure that is in the classโs scope, we delete this variable with the
unset()
function.
unset
allows you to temporarily exclude a variable, which will allow our magic
get
method to be called.
We call the private constructor
Since we have a private constructor that displays
echo
, you can just get this constructor, make it accessible by calling it.
So our secret class crumbled to pieces.
We received a message that we:
- intercepted;
- returned something completely different.
leedavis / altr-ego package
A lot of magic is already documented. The altr-ego package is just pretending to be your component.
composer show leedavis81/altr-ego --all name : leedavis81/altr-ego descrip. : Access an objects protected / private properties and methods keywords : php, break scope versions : dev-master, v1.0.2, v1.0.1, v1.0.0 type : library license : MIT License (MIT)
You can create one of your objects and attach a second to it. This will allow changes to the facility. He will change obediently and fulfill all your wishes.
Trick # 5. Immutable objects in PHP
Are there Immutable object in PHP? Yes, and a very, very long time.
namespace Magic { $object = (object) [ "\0Secret\0property" => 'test' ]; var_dump($object); }
Just get them in an interesting way. The interesting thing is that we create an array that has a special key . It starts with the construction
\0
- this is a zero byte character, and after
Secret
we also see
\0
.
The construct is used in PHP to declare a private property within a class. If we try to cast an object to an array, we will see the same keys. We will have nothing but
stdClass
. It contains a private property from the
Secret
class, which is equal to
test
.
object(stdClass) [1] private 'property' (Secret) => string 'test' (length=4)
The only problem is - then you canโt get this property from there. It is created, but not available.
I thought it was rather inconvenient - we have Immutable objects, but you cannot use it. Therefore, I decided that it was time to file my decision. I used all my knowledge and magic that is available in PHP to create a construction based on all our magic tricks.
Let's start with a simple one - create a DTO and try to intercept all the properties in it (see the previous trick).
We save in a safe place the values that we capture from there. They will be inaccessible by any methods: neither
reflection
, nor closures, nor other magic. But uncertainty arises - is there such a place in PHP that would guarantee to save some variables so that no cunning young programmer gets there at all?
We provide a magic method so that you can read this value. To do this, we have magic
getters
, magic
isset
methods that allow us to provide APIs.
Let's go back to a reliable place and try to search.
-
Global variables
canceled - anyone can change them.
-
Public properties
also not suitable.
-
Protected properties
so-so, because the child class will get through.
-
Private properties
. There is no trust, because through closure or throughreflection
it can be changed.
-
Private static properties
can be tried, butreflection
also breaks.
It would seem that there is nowhere to hide the values โโof variables. But there was a magic thing -
Static variables in functions
- these are variables that are inside functions.
Safe storage of values
I asked Nikita Popov and Jay Watkins about it on the special Stack Overflow channel.
This is a function within which a static variable is declared. Is there any way to get it out of it? The answer is no.
We have found a small loophole in the other world of protected variables and we want to use it.
Passing Values โโby Link
We will use it non-standardly, as a property of the object. But you cannot pass a property, so we use the classic transfer of values โโby reference.
It turns out that there is a class in which there is a magic
callStatic
method, and the
Static
variable is declared in it. In any call to some function, we pass the value of the variable from the Immutable object by reference to all our nested methods. So we kind of provide context.
Save state
Let's see how the state is saved.
It's pretty simple. For the transferred object, we use the
spl_object_id
function, which returns a separate identifier for each instance. The
State
that we already got from the object is trying to save there. Nothing special.
Apply the state of the object
Here again there is a construction for passing values โโby reference and
unset
properties. We unset all current properties, having previously saved them in the
State
variable, and set this context for the object. The object no longer contains any properties, but only its identifier, which is declared using
spl_object_id
and is bound to this object while it is still alive.
Get State
Then everything is simple.
We get this context on the magic
getter
and call our property from it. Now no one and nothing can change the value after this
Trait
connected.
All magic methods are overridden and implement the immutability of the object.
lisachenko / immutable-object
As expected, everything is immediately formalized in the library and ready for use.
composer show /immutable-object --all name : /immutable-object descrip. : Immutable object library keywords : versions : * dev-master type : library license : MIT License (MIT)
The library looks pretty simple. We connect it and create our class. It has different properties: private, protected and public. We connect
ImmutableTrait
.
After that, you can initiate the object once - see its value. It really is saved there and even looks like a real DTO.
object (MagicObject) [3] public 'value' => int 200
But if we try to change it, for example, like this ...
... then immediately get a
fatal exception
. We cannot change a property because it is Immutable. How so?
If you get involved in an exciting challenge and try to debase it, you get the following.
This is my present. As soon as you try to fail inside PHPStorm inside this class, it will instantly stop the execution of your command. I do not want you to delve into this code - it is too dangerous. He will warn that there is nothing to do.
Trick # 6. Thread processing
Consider the design.
include 'php://filter/read=string.toupper/resource=magic.php';
There is something magical: a PHP filter,
read
, at the end some kind of magic.php file is connected . This file looks pretty simple.
<?php echo 'Hello, world!'
Note that the case is different. However, if we โpopulateโ the file through our design, then we get this:
HELLO, WORLD!
What happened at that moment? Using the construction of the PHP filter in
include
allows you to connect any filter, including yours, to analyze the source code. You are managing everything in this source code. You can remove
final
from classes and methods, make properties public - anything you can crank through this thing.
Part of my aspect framework is based on this. When your class is connected, it analyzes it and transforms it into what will be executed.
Out of the box in PHP there is already a whole bunch of ready-made filters.
var_dump(stream_get_filters()); array (size=10) 0 => string 'zlib.*' (length=6) 1 => string 'bzip2.*' (length=7) 2 => string 'convert.iconv.*' (length=15) 3 => string ' string.rotl3' (length=12) 4 => string 'string.toupper' (length=14) 5 => string 'string.tolower' (length=14) 6 => string 'string.strip_tags' (length=17) 7 => string 'convert.*' (length=9) 8 => string 'consumed' (length=8) 9 => string 'dechunk' (length=7)
They allow you to "zip" the content, translate it into upper or lower case.
The main tricks that I wanted to show are over. Now let's move on to the quintessence of everything that I can do - aspect-oriented programming.
Trick # 7. Aspect Oriented Programming
Look at this code and think whether it is good or bad from your point of view.
The code seems to be quite adequate. It checks access rights, performs logging, creates a user, persists, tries to catch
exception
.
If you look at all this noodles, it is repeated in each of our methods, and the valuable here is only that which is marked in green.
Everything else: โsecondary concernsโ or โcrosscutting concernsโ is cross-cutting functionality.
Conventional OOP does not make it possible to copy all of these structures with copy-paste, remove them and remove them somewhere. This is bad because you have to repeat it. I wish the code always looked clean and tidy.
So that it does not contain logging (let it be applied somehow) and does not contain
security
.
So thatโs all, including logging ...
... and security checks, ...
... performed by itself.
And it is possible.
Glossary "Aspect"
There is a thing called Aspect. This is a simple example that checks permissions. Thanks to him, you can see some annotation, which is also highlighted by the plugin for PhpStorm. โAspectโ declares SQL expressions to which points in the code to apply this condition. Below, for example, we want to apply a closure to all public methods from the
UserService
class.
We get the closure using the
$invocation
method.
This is some kind of wrapper on top of the
reflection
method, which contains more arguments.
Further in this callback for each method, you can check the necessary access rights, this is called the โAdviceโ.
We kind of say the language: โDear PHP, please apply this method before every call to the public method from the
UserService
class
UserService
โ Just one line, but a lot of useful.
Aspect vs Event Listener
To make it clearer, I made a comparison of Aspect with Event Listener. Many people work in Symfony and know what Event Dispatcher is.
They are similar in that we pass some kind of dependency, for example,
AutorizationChecker
, and declare where to apply in this case. In the case of Listener, this is a
SubscrabingEvent
called
UserCreate
, and in the case of
Aspect
we subscribe to all calls to public methods from the
UserService
. At the same time, the content of the callback handler itself is approximately the same: we just check something and react accordingly.
, .
, ,
Aspects
.
. , PHP- .
, . , , OPcache.
.
Composer
. Go! AOP,
Composer
, .
Aspects
,
Aspects
. .
.
PHP-Parser . , , . , , PHP-Parser . .
. , goaop/parser-reflection .
AST- , . , , , .
.
, ,
Aspect
.
, .
, , . โ , - , .
, . , .
Aspect MOCK. ยซยป, , . .
โ
joinPoint
. -
joinPoint
: , , .
?
.
OPcache preloading for AOP Core . AOP- . 10 . Bootstrapping , PHP.
FFI integration to modify binary opcodes . , , . PHP-opcodes,
.bin
. FFI .
Modifying PHP engine internal callbacks PHP- userland. PHP . FFI PHP userland, , , . .
, .
#8. goaop/framework
, GitHub .
composer show goaop/framework --all name : goaop/framework descrip. : Framework for aspect-oriented programming in PHP. keywords : php, aop, library, aspect versions : dev-master, 3.0.x-dev, 2.x-dev, 2.3.1, โฆ type : library license : MIT License
, PhpStorm.
, Pointcuts. , , . โ , , .
Trick #9.
. , .
: , - .
fastcgi_finish_request
. , , , - callback โ .
?
,
Deffered
, , .
Aspect
, , ,
Deffered
, .
Aspect
: , , , callback, , callback. .
React ,
promise
. , - , - , .
, .
shutdown_function
Aspect
. , callback, , , , callback
onPhpTerminate
.
fastcgi_finish_request
: ยซ, , , , ยป. .
,
sendPushNotification
.
, - โ 2 .
, , , 2 .
,
Deferred
.
, . - , , .
That's all. , - . , .
PHP Russia 2020 . โ . telegram- , PHP Russia 2020.