PHP中的静态类初始化器

114

我有一个帮助类,其中包含一些静态函数。该类中的所有函数都需要运行一个“重型”初始化函数一次(就像它是构造函数一样)。

有没有好的实践方法可以实现这一点?

我所想到的唯一方法就是调用一个init函数,并在其已经运行一次时中断流程(使用静态的$initialized变量)。问题是我需要在类的每个函数中都调用它。


5
正在讨论的是 Static Class Constructor RFC,该RFC提供了一种替代方法。 - bishop
3
未来的读者:这里有代码细节和对用户 user258626 所考虑的方法进行讨论。请将其与被接受的答案进行比较,并决定哪个更适合你,或选择其他答案;我建议你不要盲目采用被接受的答案。关键点:总的原则是,在编写类时,最好付出一次编码代价,以使调用者更简单。 - ToolmakerSteve
我希望我们能够重构SO,将被接受的答案放入一个新问题“PHP中的Singleton模式是什么样子?”(对于这个问题,它是一个很好的答案),并将user258626的答案(或类似的答案)作为该问题的被接受答案。 - Adam Chalcraft
9个回答

135

看起来你最好使用单例而不是一堆静态方法。

class Singleton
{
  /**
   * 
   * @var Singleton
   */
  private static $instance;

  private function __construct()
  {
    // Your "heavy" initialization stuff here
  }

  public static function getInstance()
  {
    if ( is_null( self::$instance ) )
    {
      self::$instance = new self();
    }
    return self::$instance;
  }

  public function someMethod1()
  {
    // whatever
  }

  public function someMethod2()
  {
    // whatever
  }
}

然后,在使用时

// As opposed to this
Singleton::someMethod1();

// You'd do this
Singleton::getInstance()->someMethod1();

5
我想要在私有构造函数和getInstance()中加入-1(但我不会这样做)……这将使有效测试非常困难......请至少将其设置为受保护的,以便您有选择...... - ircmaxell
19
@ircmaxell - 你只是在谈论单例模式本身存在的问题。而且,SO上任何人发布的代码都不应被视为权威 - 特别是那些只是为了说明而编写的简单示例。每个人的情境和情况都是不同的。 - Peter Bailey
19
20整行?!?这个回答的作者不知道代码行是宝贵的资源吗?!?它们可不是随便就能得到的东西! - Peter Bailey
12
只起粘合作用但实际并没有实现功能的代码行,会分散注意力并让代码难以维护。 - Liz Av
17
@ekevoo,你知道我不是单例模式的作者。不要杀信使。 - Peter Bailey
显示剩余17条评论

104
// file Foo.php
class Foo
{
  static function init() { /* ... */ }
}

Foo::init();

这种方式可以在类文件被包含时进行初始化。通过使用自动加载,您可以确保只在必要时(且仅一次)进行初始化。


3
我不理解你的问题。所有上述情况都发生在包含的文件中。 - Victor Nicollet
1
@VictorNicollet,这很丑陋。你的代码将init作为公共方法,如果它是私有的,它将无法工作。难道没有更干净的方式,比如Java静态类初始化器吗? - Pacerier
3
如果init()函数在第二次调用时不执行任何操作,那么它是否公开的并不重要... static function init() { if(self::$inited) return; /* ... */ } - FrancescoMM
1
@Pacerier,任何接受参数的构造函数或初始化器的最终结果都是将超出范围的数据注入到类中。你必须在某个地方处理它。 - That Realty Programmer Guy
2
这与 PHP 7.4 的 opcache.preload 不兼容。如果文件在预加载脚本中预加载,则类将“存在”,但不会出现该文件中顶层代码的效果 - 并且自动加载不需要该文件,因为类已经存在,您也不需要它,因为这会导致类被重新定义! - Szczepan Hołyszewski
显示剩余2条评论

58

实际上,我在需要初始化(或至少需要执行一些代码)的静态类上使用了一个公共静态方法__init__()。然后,在我的自动加载器中,当它加载一个类时,它会检查is_callable($class, '__init__')。如果是这样,它就会调用那个方法。快速、简单而有效...


2
这也是我的建议。我过去也这样做了,但称其为__initStatic()。感觉PHP需要这样的东西,了解Java。 - Alex P
3
对于我们使用Composer的人,我找到了这个:https://packagist.org/packages/vladimmi/construct-static。 - iautomation
@iautomation 没有尝试过,但这值得放在自己的答案中!这是一种简单而现代的方法。 - robsch
对于那些在专业的生产环境中工作,其中Composer是互联网的支柱... 这个答案非常有效。 - IncredibleHat
我得到了一个错误 if(is_callable($class_name, "__init")) { $class_name::__init(); } 如果我使用 spl_autoload_register(),这个方法不能用吗? - Irvan Hilmi
如果它在类A上而不是类B上,但是B继承了A,这样不还是会被多次调用吗?当加载A时,它将被调用一次,当加载B时再次调用(因为通过继承它存在于B上)。因此,基本上,每当您加载一个继承自A的类(并且该类没有自己的__init __()方法),A__init __()方法就会再次被调用。 - Leon Williams

10

注意:这正是楼主所说的做法。(但没有展示代码)。我在这里展示细节,这样您可以将其与被接受的答案进行比较。我的观点是,我认为楼主最初的想法比他接受的答案更好。


考虑到被接受的答案受到高度赞扬,我想指出一次性初始化静态方法的“天真”答案几乎与Singleton的实现一样少——并且具有重要优势

final class MyClass  {
    public static function someMethod1() {
        MyClass::init();
        // whatever
    }

    public static function someMethod2() {
        MyClass::init();
        // whatever
    }


    private static $didInit = false;

    private static function init() {
        if (!self::$didInit) {
            self::$didInit = true;
            // one-time init code.
        }
    }

    // private, so can't create an instance.
    private function __construct() {
        // Nothing to do - there are no instances.
    }
}

这种方法的优点是,您可以使用简单直观的静态函数语法进行调用:
MyClass::someMethod1();

将其与被接受答案所要求的调用进行对比:

MyClass::getInstance->someMethod1();

作为一般原则,最好在编写类时支付编码价值以使调用者更简单。
如果你使用PHP 7.4的opcode.cache,那么请使用Victor Nicollet的答案。这很简单,不需要额外的编程。没有需要理解的“高级”编程。(我建议包括FrancescoMM的评论,以确保"init"永远不会执行两次。)请查看Szczepan的解释,了解为什么Victor的技术不适用于opcode.cache.

如果你正在使用opcode.cache,那么据我所知,我的答案最干净了。代价就是在每个公共方法的开头添加MyClass::init();这一行代码。注意:如果你想要公共属性,请将它们编写为get/set方法对,以便有地方添加那个init函数调用。

(私有成员不需要那个init函数调用,因为它们无法从外部访问-因此在执行到私有成员时,某些公共方法已经被调用过。


1
我更喜欢这种写法,因为这个类是自包含的。我只会把测试放在可能会首先被调用的方法里。请注意,test属性可以是需要使用代码进行初始化的属性,通过将其设置为false,然后当它为false时,一个方法可以调用init()来进行初始化,否则就使用该属性的值。 - Patanjali
1
@Patanjali - 我建议使用专用属性static $didInit,而不是依赖于某个特定于类的属性。这使得代码更加明显(如果您在多个类中使用相同的技术,则更加一致)。额外的内存成本是可以忽略不计的,因为它是每个类的单个静态属性(如果您创建类的实例,它不会使实例变大,而是在类本身上)。 - ToolmakerSteve
1
(@)ToolmakerSteve。如果您希望使用通用机制,我同意这种做法,因为保持一致性更好。但是,如果不是所有属性都可以同时初始化,因为类值是通过其他进程构建的,则根据需要逐个处理相关属性是可以的。 - Patanjali
1
这种方法会让维护变得困难。想一想:如果你使用init方法来设置某个属性...1)你将不得不在每个方法中保持检查它是否初始化(WET);2)如果你想要访问一个属性,你将不得不跟踪你是否已经调用了初始化属性的方法。我认为使用类的实例和构造函数的好处要好得多(换句话说,最好的答案是否定的,“不要这样做”)。 - Fabien Snauwaert
@FabienSnauwaert - 我大部分同意你的观点(正如我在回答中所说,这不是我认为最好的方法)。一些评论:1)如果您需要公共属性(而不仅仅是方法),那么您肯定超出了适用此方法的范围。2)它只是稍微有点WET;将逻辑封装在一个被多次调用并避免多次执行的标志方法中是一种经典的编程技术,早在面向对象之前就已经存在:真正的WET代码重复多个函数调用。3)像这里的大多数答案一样,这是一个解决方案,因为PHP缺少静态初始化程序。 - ToolmakerSteve
1
这种方法使MyClass稍微变得冗长,但它使每个调用MyClass的人更加简洁,因为他们不需要写MyClass::getInstance->someMethod1();。在维护MyClass时,代码就在你面前,所以如果必须要有一些冗长,那么应该是在这里。 - Adam Chalcraft

8

有一种方法可以调用init()方法一次并禁止它的使用,您可以将该函数转换为私有初始化程序,并在类声明后调用它,如下所示:

class Example {
    private static function init() {
        // do whatever needed for class initialization
    }
}
(static function () {
    static::init();
})->bindTo(null, Example::class)();

4
这看起来异常有趣。 - emfi
它是如何能够从类外部调用private init的?你能解释一下你在这里做什么的细节吗? - ToolmakerSteve
1
@ToolmakerSteve 根据文档的说法,_"静态闭包不能有任何绑定对象(参数newthis的值应为NULL),但是此函数仍然可以用于更改它们的类作用域。"_ 这就是为什么闭包作用域绑定到Example::class,因此可以调用私有方法。我发现一个错误,因为init()方法应该是static - 已经修复了示例。 - brzuchal
2
实际上,我们甚至不需要 init() 方法,也就是说,我们可以将所有的初始化代码直接放入这个匿名函数中,它本身就可以充当静态构造函数。 - Karolis
2
注意事项:请参考Szczepan的回答,该回答解释了如果使用PHP 7.4的opcache.preload机制,此类技术将无法正常工作。 - ToolmakerSteve
显示剩余3条评论

4

我发布这篇文章作为答案,因为从PHP 7.4开始,这非常重要。

PHP 7.4的opcache.preload机制可以预加载类的操作码。如果使用它来预加载包含类定义某些副作用的文件,那么在该FPM服务器及其工作进程执行的所有后续脚本中都会“存在”于该文件中定义的类,但是副作用将不会生效,并且自动加载程序将不需要包含它们的文件,因为该类已经“存在”。这完全破坏了任何依赖于在包含类定义的文件中执行顶层代码的静态初始化技术。


0

分配静态公共属性的一些测试:

settings.json:

{
    "HOST": "website.com",
    "NB_FOR_PAGINA": 8,
    "DEF_ARR_SIZES": {
        "min": 600,
        "max": 1200
    },
    "TOKEN_TIME": 3600,
    "WEBSITE_TITLE": "My website title"
}

现在我们想要向我们的类添加公共静态属性设置

class test {
  
  /**  prepare an array to store datas  */
  public static $datas = array();
  
 /**
  * test::init();
  */
  public static function init(){
    
    // get json file to init.
    $get_json_settings = 
      file_get_contents(dirname(__DIR__).'/API/settings.json');

    $SETTINGS = json_decode($get_json_settings, true);
                
    foreach( $SETTINGS as $key => $value ){
         
       // set public static properties
       self::$datas[$key] = $value;         
    }

  }
 /**
  * 
  */


 /**
  * test::get_static_properties($class_name);
  *
  * @param  {type} $class_name
  * @return {log}  return all static properties of API object
  */
  public static function get_static_properties($class_name) {

    $class = new ReflectionClass($class_name);

    echo '<b>infos Class : '.$class->name.'</b><br>';

    $staticMembers = $class->getStaticProperties();

    foreach( $staticMembers as $key => $value ){

        echo '<pre>';
        echo $key. ' -> ';

        if( is_array($value) ){
            var_export($value);
        }
        else if( is_bool($value) ){

            var_export($value);
        }
        else{

            echo $value;
        }

        echo '</pre>';

    }
    // end foreach

  }
 /**
  * END test::get_static_properties();
  */

}
// end class test

好的,现在我们测试这段代码:

// consider we have the class test in API folder
spl_autoload_register(function ($class){
    
    // call path to API folder after
    $path_API = dirname(__DIR__).'/API/' . $class . '.php';
    
    if( file_exists($path_API) ) require $path_API;
});
// end SPL auto registrer

// init class test with dynamics static properties 
test::init();
test::get_static_properties('test');
var_dump(test::$HOST);
var_dump(test::$datas['HOST']);

这将返回:

infos Class : test

datas -> array (
  'HOST' => 'website.com',
  'NB_FOR_PAGINA' => 8,
  'DEF_ARR_SIZES' => 
  array (
    'min' => 600,
    'max' => 1200,
  ),
  'TOKEN_TIME' => 3600,
  'WEBSITE_TITLE' => 'My website title'
)

// var_dump(test::$HOST);
Uncaught Error: Access to undeclared static property: 
test::$HOST
// var_dump(test::$datas['HOST']);
website.com

然后,如果我们像这样修改test类:

    class test {
      
      /**  Determine empty public static properties  */
      public static $HOST;
      public static $NB_FOR_PAGINA;
      public static $DEF_ARR_SIZES;
      public static $TOKEN_TIME;
      public static $WEBSITE_TITLE;
      
     /**
      * test::init();
      */
      public static function init(){
        
        // get json file to init.
        $get_json_settings = 
          file_get_contents(dirname(__DIR__).'/API/settings.json');
    
        $SETTINGS = json_decode($get_json_settings, true);
                    
        foreach( $SETTINGS as $key => $value ){
             
           // set public static properties 
           self::${$key} = $value;                  
        }
    
      }
     /**
      * 
      */
...
}
// end class test 

// init class test with dynamics static properties 
test::init();
test::get_static_properties('test');
var_dump(test::$HOST);

这个返回:

infos Class : test
    
  HOST -> website.com
  NB_FOR_PAGINA -> 8
  DEF_ARR_SIZES -> array (
  'min' => 600,
  'max' => 1200,
)
TOKEN_TIME -> 3600
WEBSITE_TITLE -> My website title

// var_dump(test::$HOST);
website.com

实际上,我需要初始化一个具有公共静态属性的对象,我将在许多其他类中重复使用。我认为这应该是可以实现的,我不想在每个需要它的方法中都做new api(),比如检查站点主机或指示站点主机。另外,我希望使事情更加动态化,这样我就可以添加尽可能多的设置到我的API中,而无需在初始化类中声明它们。所有其他方法我见过的都不再适用于php > 7.4,我一直在寻找解决这个问题的办法。


0
如果您不喜欢使用 public 静态初始化器,反射可以是一种解决方法。
<?php

class LanguageUtility
{
    public static function initializeClass($class)
    {
        try
        {
            // Get a static method named 'initialize'. If not found,
            // ReflectionMethod() will throw a ReflectionException.
            $ref = new \ReflectionMethod($class, 'initialize');

            // The 'initialize' method is probably 'private'.
            // Make it accessible before calling 'invoke'.
            // Note that 'setAccessible' is not available
            // before PHP version 5.3.2.
            $ref->setAccessible(true);

            // Execute the 'initialize' method.
            $ref->invoke(null);
        }   
        catch (Exception $e)
        {
        }
    }
}

class MyClass
{
    private static function initialize()
    {
    }
}

LanguageUtility::initializeClass('MyClass');

?>

1
这与 PHP 7.4 的 opcache.preload 不兼容;请参考我的答案。 - Szczepan Hołyszewski

-2

6
这份RFC尚未通过草案阶段,请不要链接或提供未经投票批准的事例。这会使新用户感到困惑,他们可能没有意识到这些内容目前还不能使用。 - Machavity

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接