魔术学院

PHP中的魔术是什么? 这通常意味着_construct()__get() 。 PHP中的魔术方法是漏洞,可帮助开发人员完成出色的工作。 网络中充满了您可能熟悉的使用说明。 但是,如果我们说您还没有看到真正的魔法该怎么办? 毕竟,您似乎对所有事情都了解的越多,魔术越难以捉摸。



让我们放弃已建立的OOP规则框架,并在PHP魔术学校中使不可能变为可能。 学校的主要魔法老师是亚历山大·利萨琴科NightTiger )。 他将教授魔术思维,也许您会喜欢魔术方法,访问属性的非标准方法,更改上下文,面向方面的编程和流过滤器。


Alexander Lisachenko是Alpari网站开发和架构部门的负责人。 面向方面的框架Go!的作者和首席开发人员 Aop 。 在PHP国际会议上的演讲者。

在好电影《欺骗的幻觉》中,有一个短语:
“你离得越近,看到的东西就越少。”

关于PHP,可以说是一样的,它是一种魔术,使您可以做一些不寻常的事情。 但首先,它是为了欺骗您而创建的:“ ...旨在欺骗他人的行为,既可以作为欺骗某人的方式,也可以作为一种玩笑或娱乐形式。”

如果我们一起使用PHP并尝试编写一些神奇的东西,很可能我会欺骗您。 我会做一些技巧,您很长一段时间以来都会想知道为什么会这样。 这是因为PHP是一种以其不寻常的东西而闻名的编程语言。

魔术装备


我们需要魔术装备什么? 痛苦的熟悉方法。

__construct(), __destruct(), __clone(),
__call(), __callStatic(),
__get(), __set(), __isset(), __unset(),
__sleep(), __wakeup(),
__toString(), __invoke(), __set_state(),
__debugInfo()


我将分别说明最后一种方法-有了它,您就可以解决不寻常的事情。 但这还不是全部。

  • declare(ticks=1)
  • debug_backtrace() 。 这是我们的同伴,以了解我们在当前代码中的位置,了解谁给我们打电话,为什么给我们打电话,以及带有什么参数。 决定不遵循所有逻辑很有用。
  • unset(), isset() 。 似乎没什么特别的,但是这些设计隐藏了很多技巧,我们将进一步考虑这些技巧。
  • by-reference passing 。 当我们通过引用传递某些对象或变量时,我们应该期望不可避免地会发生一些魔术。
  • bound closures 。 关于闭包以及它们可以绑定的事实,您可以构建很多技巧。
  • Reflection API有助于将反射提高到一个新的水平。
  • StreamWrapper API。

设备已经准备就绪-我会提醒魔术的第一法则。

永远是房间里最聪明的人。

绝招#1。 不可能的比较


让我们从第一个技巧开始,我将其称为“不可能比较”。
仔细看一下代码,看看这是否可能在PHP中发生。



有一个变量,我们声明它的值,然后突然它不等于自己。

非数字


有一个不可思议的人工产物,例如NaN-非数字。



它的惊人功能是它不等于自身。 这是我们的第一个技巧:使用NaN迷惑一个朋友。 但是,NaN并不是此任务的唯一解决方案。

我们使用常量




诀窍是我们可以将namespace false声明为true并进行比较。 不幸的开发人员会很长一段时间想知道为什么存在false

处理程序


下一个招数是比前两个选项更强大的火炮。 让我们看看它是如何工作的。



技巧是基于tick_function并且正如我提到的, declare(ticks=1)

开箱即用怎么办? 首先,我们声明一些函数,并通过引用采用isMagic参数,然后尝试将该值更改为true 。 在我们声明了declare(ticks=1) ,PHP解释器在每个基本操作之后调用register_tick_function回调。 在此回调中,我们可以将以前为false的值更改为true 。 魔术!

绝招#2。 魔术表情


以声明两个变量的示例为例。 其中一个是false ,另一个是true 。 我们isBlackisWhite和var_dump结果。 您认为结果如何?



优先运营商 。 正确答案是false ,因为在PHP中存在诸如“运算符优先级”之类的东西。



令人惊讶的是,逻辑运算符or优先级低于赋值操作。 因此,简单地分配false 。 在isWhite如果第一部分失败isWhite可以执行任何其他表达式。

魔术表情


看下面的代码。 有一些包含构造函数的类,还有一些工厂,其代码将更进一步。



注意最后一行。

 $value = new $factory->build(Foo::class); 

有几种选择可能发生。

  • $factory将被用作new的类名;
  • 会有解析错误;
  • 将使用调用$factory->build() ,此函数将返回其值,结果将为new
  • $factory->build属性的值将用于构造类。

让我们看看最后一个想法。 在$factory类中,我们声明了一系列魔术函数。 他们将编写我们的工作:调用属性,调用方法,甚至尝试在对象上调用invoke

 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;} } 

正确答案:我们不是在调用方法,而是在调用属性。 在$factory->build有一个用于构造函数的参数,该参数将传递给此类。

顺便说一下,在这个框架中,我实现了一个称为“拦截新对象的创建”的功能-您可以“锁定”该设计。

解析器漏洞


以下示例适用于PHP解析器本身。 您是否尝试过在花括号内调用函数或分配变量?



我在Twitter上获得了这个技巧,它的工作非常不规范。

 $result = ${'_' . !$_=getCallback()}(); $_=getCallback(); // string(5) "hello" !$_=getCallback()}(); // bool(false) '_'.!$_=getCallback()}(); // string(1) "_" ${'_'.!$_=getCallback()}(); // string(5) "hello" 

首先,我们将表达式的值分配给名为_ (下划线)的变量。 我们已经有一个变量,尝试在逻辑上将其值求反,然后我们得到false该行强制转换为true 。 然后,将它们全部粘贴到变量名中,然后将其放在大括号内。

什么是魔术? 这是一种娱乐活动,使我们感到鼓舞,不寻常,说:“什么? 所以有可能吗?!

绝招#3。 违反规则


我对PHP的满意之处在于,您可以打破每个人创建的规则,以使其变得超级安全。 有一种称为“密封类”的设计,该设计具有私有构造函数。 作为魔术师的学生,您的任务是创建此类的一个实例。



考虑如何执行此操作的三个选项。

解决方法


第一种方法是最明显的。 每个开发人员都应该熟悉-这是该语言为我们提供的标准API。



newInstanceWithoutConstructor构造允许您绕过构造函数是私有的语言限制,并绕过我们所有的私有构造函数声明创建类的实例。

该选项有效,简单,不需要任何解释。

短路


第二种选择需要更多的关注和技巧。 创建一个匿名函数,然后将其绑定到该类的鱼鹰。



在这里,我们位于类内部,可以安全地调用私有方法。 我们通过在类的上下文中调用new static来使用它。



反序列化


我认为,第三个选项是最高级的。



如果您以特定格式编写特定行,请在其中替换某些值-我们的班级将会出来。



反序列化之后,我们得到instance

教义/实例化程序包


Magic通常会成为文档化的框架或库-例如,在学说/实例化器中,所有这一切都得以实现。 我们可以用任何代码创建任何对象。

 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) 

绝招#4。 拦截财产访问


乌云密布:类是秘密的,属性和构造函数是私有的,还有回调。

 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? 

作为向导,我们的任务是以某种方式调用回调。

添加魔术...吸气剂


添加一些魔法使其起作用。



这种魔法是一种神奇的getter 。 它调用一个函数,到目前为止没有发生任何可怕的事情。 但是,我们将使用前面的技巧,并绕过私有构造创建此对象的实例。



现在我们需要以某种方式调用回调。

电路内部“未设置”


为此,创建一个闭合。 在类作用域内的闭包内部,我们使用unset()函数删除此变量。



unset允许您临时排除变量,这将允许调用我们的magic get方法。

我们称私有构造函数


由于我们有一个显示echo的私有构造函数,您只需获取此构造函数,即可通过调用它使其可访问。



因此,我们的秘密班级崩溃了。



我们收到一条消息,我们:

  • 被拦截
  • 返回了完全不同的东西。

leedavis / altr-ego软件包


已经记录了很多魔术。 altr-ego软件包只是伪装成您的组件。

 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) 

您可以创建一个对象,然后再添加一个对象。 这将允许更改设施。 他会乖乖地改变并实现您的所有愿望。

绝招#5。 PHP中的不可变对象


PHP中有不可变对象吗? 是的,而且非常非常长的时间。

 namespace Magic { $object = (object) [ "\0Secret\0property" => 'test' ]; var_dump($object); } 

以一种有趣的方式获得它们。 有趣的是,我们创建了一个具有特殊键的数组。 它从结构\0 -这是一个零字节字符,在Secret之后,我们也看到\0

PHP中使用该构造在类中声明私有属性。 如果尝试将对象强制转换为数组,则会看到相同的键。 除了stdClass我们什么都没有。 它包含Secret类的私有属性,该属性等于test

 object(stdClass) [1] private 'property' (Secret) => string 'test' (length=4) 

唯一的问题是-那么您无法从那里获得此属性。 它已创建,但不可用。

我认为这很不方便-我们有不可变的对象,但是您不能使用它。 因此,我认为是时候提出我的决定了。 我使用了PHP中提供的所有知识和魔术,根据所有魔术技巧创建了一个结构。

让我们从一个简单的例子开始-创建一个DTO并尝试截取其中的所有属性(请参阅前面的技巧)。

我们将从那里捕获的值保存在安全的地方 。 它们将无法通过任何方法访问: reflection ,闭包或其他魔术方法。 但是存在不确定性-PHP中是否有这样的地方可以保证保存一些变量,以便没有一个狡猾的年轻程序员能到达那里?

我们提供了一种魔术方法,以便您可以读取此值。 为此,我们有魔术getters ,魔术isset方法,这些方法使我们能够提供API。

让我们回到一个可靠的地方,尝试搜索。

  • Global variables取消-任何人都可以更改它们。
  • Public properties也不适合。
  • Protected properties的,因为子类将通过。
  • Private properties 。 没有信任,因为可以通过关闭或reflection来更改它。
  • 可以尝试使用Private static properties ,但是reflection也会中断。

似乎没有地方可以隐藏变量的值。 但是有一件神奇的事情- Static variables in functions -这些是函数内部的变量。

安全存储价值


我在特殊的Stack Overflow频道上询问了Nikita PopovJay Watkins



这是一个在其中声明静态变量的函数。 是否有可能摆脱它,改变它? 答案是否定的。

我们在另一个受保护的变量世界中发现了一个小漏洞,我们想使用它。

通过链接传递值


我们将非标准地将其用作对象的属性。 但是您不能传递属性,因此我们通过引用使用经典的值传递。



事实证明,有一个类中有一个神奇的callStatic方法,并在其中声明了Static变量。 在对某个函数的任何调用中,我们都通过引用所有嵌套方法来传递Immutable对象中变量的值。 因此,我们提供了上下文。

保存状态


让我们看看状态如何保存。



很简单 对于传输的对象,我们使用spl_object_id函数,该函数为每个实例返回一个单独的标识符。 我们已经从对象获得的状态正在尝试保存在那里。 没什么特别的

应用对象的状态


同样,这里有一种通过引用和未unset属性传递值的构造。 我们取消了所有当前属性,之前已将它们保存在State变量中,然后为对象设置此上下文。 该对象不再包含任何属性,而仅包含其标识符,该标识符使用spl_object_id声明,并在该对象仍然存在时附加到该对象。



获取状态


然后,一切都很简单。



我们在魔术获取器上获取此上下文,并从中调用我们的属性。 现在,连接此Trait后,没有人可以更改值。



所有魔术方法都将被覆盖并实现对象的不变性。



利萨琴科/不可变对象


正如预期的那样,所有内容都会立即在库中正式化并可以使用。

 composer show /immutable-object --all name : /immutable-object descrip. : Immutable object library keywords : versions : * dev-master type : library license : MIT License (MIT) 

该库看起来很简单。 我们将其连接并创建我们的课程。 它具有不同的属性:私有,受保护的和公共的。 我们连接ImmutableTrait



之后,您可以一次初始化对象-查看其值。 它确实保存在那里,甚至看起来像一个真正的DTO。

 object (MagicObject) [3] public 'value' => int 200 

但是,例如,如果我们尝试更改它,例如...



...然后立即得到fatal exception 。 我们无法更改属性,因为它是不可变的。 怎么会这样



如果您参与了一个激动人心的挑战并试图降低它的竞争力,您将获得以下收益。



这是我的礼物。 一旦您尝试在此类的PHPStorm中失败,它将立即停止命令的执行。 我不希望您深入研究此代码-这太危险了。 他将警告没有事可做。

绝招#6。 线程处理


考虑设计。

 include 'php://filter/read=string.toupper/resource=magic.php'; 

有一个神奇的东西:一个PHP过滤器, read ,最后连接了某种magic.php 文件 。 这个文件看起来很简单。

 <?php echo 'Hello, world!' 

请注意,情况有所不同。 但是,如果我们通过设计“填充”文件,则会得到以下信息:

 HELLO, WORLD! 

那一刻发生了什么事? 使用include PHP过滤器的构造,您可以连接任何过滤器(包括您自己的过滤器)来分析源代码。 您正在管理此源代码中的所有内容。 您可以从类和方法中删除final ,将属性设为公共-您可以通过此方法进行处理。

我的方面框架的一部分是基于此的。 连接您的类后,它将对其进行分析并将其转换为将要执行的内容。

PHP开箱即用,已经有一大堆现成的过滤器。

 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) 

它们允许您“压缩”内容,将其转换为大写或小写。

我想展示的主要技巧已经结束。 现在,让我们继续我能做的所有事情的精髓-面向方面的编程。

绝招#7。 面向方面的编程


查看此代码,并从您的角度考虑它是好是坏。



该代码似乎已经足够了。 它检查访问权限,执行日志记录,创建用户,保留,尝试捕获exception

如果您查看所有这些面条,则在我们的每种方法中都将其重复,这里唯一有价值的是将其标记为绿色。



其他所有内容:“次要关注点”或“跨领域关注点”是跨领域功能。

传统的OOP使得无法使用复制粘贴复制所有这些结构,将它们删除并将它们带到某个地方。 这很糟糕,因为您必须重复此操作。 我希望代码总是看起来整洁。



因此它不包含日志记录(以某种方式应用它)并且不包含security



以上就是所有内容,包括日志记录...



...和安全检查,...



...自己表演。

这是可能的。

术语表“方面”


有一个东西叫做Aspect。 这是一个检查权限的简单示例。 多亏了他,您可以看到一些注释,PhpStorm的插件也突出显示了该注释。 “ Aspect”声明SQL表达式,代码中要对此条件应用的点。 例如,在下面,我们要对UserService类中的所有公共方法应用闭包。



我们使用$invocation方法获得闭包。



这是reflection方法顶部的一种包装,其中包含更多参数。

此外,在每种方法的此回调中,您可以检查必要的访问权限,这称为“建议”。



我们用这种语言来表达:“亲爱的PHP,请在每次从UserService类调用公共方法之前应用此方法UserService ” 只需一行,但很有用。

方面与事件侦听器


为了更加清楚,我将Aspect与Event Listener进行了比较。 许多人在Symfony工作,并且知道什么是Event Dispatcher。



它们的相似之处在于,我们传递了某种依赖关系,例如AutorizationChecker ,并声明了这种情况下的应用位置。 在Listener的情况下,这是一个名为UserCreate ;在Aspect的情况下Aspect我们从UserService预订了对公共方法的所有调用。 callback : - .

, .

, , 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 .



, . - , , .

仅此而已。 , - . , .

PHP Russia 2020 . — . telegram- , PHP Russia 2020.

Source: https://habr.com/ru/post/zh-CN478618/


All Articles