PHP 中的静态类初始化器

我有一个带有一些静态函数的 helper 类。类中的所有函数都需要一个“重”初始化函数来运行一次(就像它是一个构造函数一样)。

有没有实现这一目标的良好做法?

我唯一想到的就是调用一个 init函数,如果它已经运行过一次,就中断它的流(使用一个静态 $initialized变量)。问题是我需要在类的每个函数上调用它。

116434 次浏览

Sounds like you'd be better served by a singleton rather than a bunch of static methods

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

And then, in usage

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


// You'd do this
Singleton::getInstance()->someMethod1();
// file Foo.php
class Foo
{
static function init() { /* ... */ }
}


Foo::init();

This way, the initialization happens when the class file is included. You can make sure this only happens when necessary (and only once) by using autoloading.

Actually, I use a public static method __init__() on my static classes that require initialization (or at least need to execute some code). Then, in my autoloader, when it loads a class it checks is_callable($class, '__init__'). If it is, it calls that method. Quick, simple and effective...

Note - the RFC proposing this is still in the draft state.


class Singleton
{
private static function __static()
{
//...
}
//...
}

proposed for PHP 7.x (see https://wiki.php.net/rfc/static_class_constructor )

If you don't like public static initializer, reflection can be a workaround.

<?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');


?>

There is a way to call the init() method once and forbid it's usage, you can turn the function into private initializer and ivoke it after class declaration like this:

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

NOTE: This is exactly what OP said they did. (But didn't show code for.) I show the details here, so that you can compare it to the accepted answer. My point is that OP's original instinct was, IMHO, better than the answer he accepted.


Given how highly upvoted the accepted answer is, I'd like to point out the "naive" answer to one-time initialization of static methods, is hardly more code than that implementation of Singleton -- and has an essential advantage.

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

The advantage of this approach, is that you get to call with the straightforward static function syntax:

MyClass::someMethod1();

Contrast it to the calls required by the accepted answer:

MyClass::getInstance->someMethod1();

As a general principle, it is best to pay the coding price once, when you code a class, to keep callers simpler.


If you are NOT using PHP 7.4's opcode.cache, then use Victor Nicollet's answer. Simple. No extra coding required. No "advanced" coding to understand. (I recommend including FrancescoMM's comment, to make sure "init" will never execute twice.) See Szczepan's explanation of why Victor's technique won't work with opcode.cache.

If you ARE using opcode.cache, then AFAIK my answer is as clean as you can get. The cost is simply adding the line MyClass::init(); at start of every public method. NOTE: If you want public properties, code them as a get / set pair of methods, so that you have a place to add that init call.

(Private members do NOT need that init call, as they are not reachable from the outside - so some public method has already been called, by the time execution reaches the private member.)

I am posting this as an answer because this is very important as of PHP 7.4.

The opcache.preload mechanism of PHP 7.4 makes it possible to preload opcodes for classes. If you use it to preload a file that contains a class definition and some side effects, then classes defined in that file will "exist" for all subsequent scripts executed by this FPM server and its workers, but the side effects will not be in effect, and the autoloader will not require the file containing them because the class already "exists". This completely defeats any and all static initialization techniques that rely on executing top-level code in the file that contains the class definition.

Some tests of assigning static public properties :

settings.json :

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

now we want to add settings public static properties to our class

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

ok now we test this code :

// 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']);

this return :

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

Then if we modify the class test like this :

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

this return :

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

I actually need to initialize an object with public static properties that I will reuse in many other classes, which I think is supposed to, I don't want to do new api() in every method where I would need, for example to check the host of the site or indicate it. Also I would like to make things more dynamic so that I can add as many settings as I want to my API, without having to declare them in my initialization class. All other methods I've seen no longer work under php > 7.4 I keep looking for a solution for this problem.