neunerlei/lockpick

一组工具,帮助你在编码时克服一些任意的限制

0.0.10 2023-03-05 21:26 UTC

This package is auto-updated.

Last update: 2024-09-06 01:19:19 UTC


README

TLDR

这是一个允许您禁用项目中使用的代码中其他作者可能施加的所有锁的包。它包含了我多年来一直在使用的某些工具,效果相当不错。

警告

  • 预计会有大量的反射和字符串操作 🙈
  • 如果您打开第三方代码,请确保您知道自己在做什么,并且代码可能会更改,因此您的代码需要足够灵活,以免出错。

闲聊

虽然我理解SOLID等原则存在的原因,以及为什么修改第三方代码容易遇到麻烦。在日常工作中,当你需要找到解决方案时,每次看到privatefinal这两个词,都会让我感到尴尬。在其他开发者可能使用的代码中,将某物设置为私有的且最终的,就像在没有灯光、窗户和出口的门房间里锁住某人一样。如果您作为作者使用protected,并且有人修改了该代码,如果没有明确标记为"@api",那么应该很清楚可能会有很多麻烦。但是,请,请全球的各位男女,不要剥夺别人修复错误或扩展您所做的功能的选择。

安装

使用composer安装此包

composer require neunerlei/lockpick

包含的内容

lock-pick类

Neunerlei\Lockpick\Util\ClassLockpick类是一个最小侵入性的工具,用于处理被锁定扩展的对象。例如,如果您想从一个私有属性中获取数据,或者调用一个私有方法。使用简单,开销最小

<?php

use Neunerlei\Lockpick\Util\ClassLockpick;

class LockedClass {
    private static int $staticProperty = 0;
    private int $property = 1;
    
    private function foo(string $name): string{
        return sprintf('hello %s', $name);
    }
    
    private static function staticFoo(): string {
        return 'I am static!';
    }
}

$i = new LockedClass();

// Create a lock-pick instance for the class
$lp = new ClassLockpick($i);

// Check if the property exists
if($lp->hasProperty('property')){
    // Use one of the utility methods
    echo $lp->getPropertyValue('property'); // 1
    
    // You can set the value in the same way
    $lp->setPropertyValue('property', 3);
    
    // Alternatively you can use the "magic" approach
    echo $lp->property; // 3
    $lp->property = 5;
    echo $lp->property; // 5
}

// You can do the same for methods
if($lp->hasMethod('foo')){
    // You can also run parameters in it
    echo $lp->runMethod('foo', ['bar']); // 'hello bar'
    
    // You can also apply a bit of sugar here
    echo $lp->foo('bar'); // 'hello bar'
}

// Of course, this also works for statics
if(ClassLockpick::hasStaticProperty(LockedClass::class, 'staticProperty')){
    echo ClassLockpick::getStaticPropertyValue(LockedClass::class, 'staticProperty'); // 0;
    ClassLockpick::setStaticPropertyValue(LockedClass::class, 'staticProperty' 2);
    echo ClassLockpick::getStaticPropertyValue(LockedClass::class, 'staticProperty'); // 2;
}

// This will work for methods as well
if(ClassLockpick::hasStaticMethod(LockedClass::class, 'staticFoo')){
    echo ClassLockpick::runStaticMethod(LockedClass::class, 'staticFoo'); // 'I am static'
}

一些建议

虽然可以使用ClassLockpick,但您应该始终问自己是否真的需要它。如果您想访问另一个对象的受保护属性或方法,您总是可以使用适配器类,如下所示

<?php

class SomeClass {
    protected int $property = 1;
    
    protected function method(): string{
        return 'hello';
    }
}

class SomeClassAdapter extends SomeClass {
    public static function getInstanceProperty(SomeClass $instance): int {
        return $instance->property;
    }
    
    public static function runInstanceMethod(SomeClass $instance): string {
        return $instance->method();
    }
}

$i = new SomeClass();

echo SomeClassAdapter::getInstanceProperty($i); // 1
echo SomeClassAdapter::runInstanceMethod($i); // 'hello' 

这样,您的代码不依赖于反射,对您的IDE来说易于解析(读取),并允许未来的更改进行扩展。

类覆盖器

现在,让我们来看看更强大的工具,好吗?假设您需要/想要扩展类的功能,或者在不分支整个包的情况下挂钩现有进程,但一切都被设置为finalprivate?在这种情况下,唯一的解决方案是修改类的实际代码以打破它们。Class Overrider是一个运行时工具,可以自动覆盖类。

安装

安装很简单(但有点复杂),但您必须了解您正在与之工作的应用程序。

  1. 您需要知道一个可以安全存储编译的PHP类的位置。(docroot之外的可写目录)
  2. 您希望在应用程序的生命周期中尽早配置覆盖,以便充分利用此功能。
  3. 您的应用程序需要使用composer运行

例如,在一个Symfony应用程序中,我建议在Kernel的“boot”方法的顶部执行此操作。作为存储位置,我建议使用应用程序的“var”目录,最好是在一个子目录中,如/var/classOverrides咳嗽或使用Symfony bundle

<?php

use Neunerlei\Lockpick\Override\ClassOverrider;

// First you need to register the class overrider for your application
ClassOverrider::init(
    // You need to provide an autoloader, which can be either done completely manually,
    // or using the "makeAutoLoaderByStoragePath" factory.
    ClassOverrider::makeAutoLoaderByStoragePath(
        // The first parameter is the absolute path to the directory where we can put some PRIVATE PHP FILEs
        // This directory MUST be writable by the webserver, but MUST NEVER be readable to the outside world!
        __DIR__.'/var/classOverrides',
        // The second parameter is the composer autoloader, which you can acquire by "requiring" the
        // autoload.php file from the vendor directory. There is no harm in doing this multiple times,
        // so it will work fine, even if composer was already loaded earlier. 
        require __DIR__ . '/../vendor/autoload.php'
    )
);

如果您运行了应用程序(或刷新了页面),并且一切正常,没有任何问题,那么您可以继续进行。

用法

在您的应用程序中安装了Class Overrider之后,您需要考虑两条规则

  1. 确保您想要覆盖的类尚未通过自动加载或直接包含加载
  2. 您的类必须可以使用Composer的自动加载功能进行加载

如果您的类符合条件,您可以在代码中这样调用覆盖器;想象一下,您有一个类似于以下这样的类,它来自第三方包

<?php
namespace ForeignVendor\ForeignNamespace;
final class TargetClass {
    public function foo(){
        // Returns interesting stuff
        return 'foo';
    }

    private function privateBar() {
        // Does fancy stuff
    }
}

要扩展这个类,您首先需要在您的代码中创建一个新的类,这就是您的扩展。位置和命名空间由您决定,您唯一需要做的就是扩展SpecialParentClass™。

SpecialParentClass™将会根据您想要覆盖的类的名称为您自动生成(在您完成了所有步骤之后)。

因此,对于我们的例子:ForeignVendor\ForeignNamespace\TargetClass,SpecialParentClass™将被命名为ForeignVendor\ForeignNamespace\LockpickClassOverrideTargetClass,而不是原来的。LockpickClassOverride部分将是每个生成的类的名称前缀。

重要:在创建扩展类时,SpecialParentClass™可能不存在,因此您不能依赖IDE的自动完成功能。但是,当代码尝试访问它时,它将存在。

<?php
namespace YourVendor\YourNamespace;
use ForeignVendor\ForeignNamespace\LockpickClassOverrideTargetClass;
class ExtendedTargetClass extends LockpickClassOverrideTargetClass {
    public function foo(?bool $useExtension = null){
        // Use private members of the parent without problems
        $this->privateBar();
        
        // You can implement your own features
        if($useExtension !== false){
            return 'bar';
        }
        
        // Or can call the parent implementation without problems 
        return parent::foo();
    }
}

之后,您可以在代码中的某个地方调用Class Overrider,在实际实现包含之前。我建议在调用ClassOverrider::init附近配置覆盖器。

<?php
use Neunerlei\Lockpick\Override\ClassOverrider;

use ForeignVendor\ForeignNamespace\TargetClass;
use YourVendor\YourNamespace\ExtendedTargetClass;

ClassOverrider::registerOverride(TargetClass::class, ExtendedTargetClass::class);

现在您能够像什么都没有发生一样创建类的实例

<?php

use ForeignVendor\ForeignNamespace\TargetClass;
$i = new TargetClass();

然而,尽管它看起来像TargetClass,但它已经不再完全像TargetClass了。魔法已经发生,您甚至没有注意到;如果您执行echo $i->foo(),结果现在将是"bar",而不是"foo"。自动加载器(或OverrideStackResolver)为您添加了两个文件

  • 首先;"clone",或者说SpecialParentClass™,基本上是TargetClass的一个副本,但将所有属性、方法和常量转换为"protected"(如果之前是私有的)。类或方法签名中的"final"修饰符也被移除。
  • 其次;"alias",它创建了一个名为ForeignVendor\ForeignNamespace\TargetClass的空壳,该空壳扩展了您的扩展类。

有了这些,代码的每个部分现在都将使用您的实现/扩展而不是原始类。

安装的第二部分 - 或者如何去除副本

因为我们实际上在驱动器上生成文件并重复使用它们以避免性能问题,所以您可能希望删除更新的文件。

我建议在composer安装/更新或在您的框架清除其缓存时执行此操作。

在您初始化覆盖器后,调用ClassOverrider::flushStorage();方法以删除所有编译的类副本。

注意事项

  • 扩展的类是原始类的修改副本,因此您的IDE shift-click将不会按预期工作。
  • 在日志和堆栈跟踪中,您将看到扩展的类和复制的类名称,而不是原始类。
  • 仅适用于遵循PSR-4指南的类,每个文件只有一个类
  • 如果您正在使用PHP预加载(特别是在Symfony中),可能会引起问题,您需要以某种方式删除在ClassOverrider::getNotPreloadableClasses();中找到的所有类,具体取决于您的框架

框架集成

明信片软件

您可以使用此软件包,但如果它进入了您的生产环境,我将非常感谢您从您的家乡寄给我一张明信片,并提到您正在使用我们的哪些软件包。

您可以在以下链接找到我的地址:这里

谢谢 :D