In CodeIgniter, how can I pass parameters from the context where the hook is called?
Example:
File_Where_Hook_is_Called.php:
$this->hooks = load_class('Hooks', 'core');
$filename = 'example.zip';
$this->hooks->call_hook('site_export_before_delete');
config/hooks.php:
$hook['site_export_before_delete'][] = array(
'class' => 'Foo',
'function' => 'export_site',
'filename' => 'Foo.php',
'filepath' => 'modules/foo/controllers',
'params' => [''] # Should be $filename from File_Where_Hook_is_Called.php
);
application/modules/foo/controllers/Foo.php:
public function export_site($filename) {
echo $filename); # Should print $filename from File_Where_Hook_is_Called.php
}
I know it may be a bit late but I stumbled upon your question as I had the same need and I solved it in a correct manner.
According to the CodeIgniter documentation you can override the default CI_Hooks class behavior by implementing your own MY_Hooks class, usually at application/core/MY_Hooks.php
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class MY_Hooks extends CI_Hooks{
protected function _call_single_hook($data, $params) {
if($params) {
if(!isset($data['params'])) {
$data['params'] = [];
}
$data['params'] = array_merge($data['params'], $params);
}
$this->_run_hook($data);
}
/**
* Call Hook
*
* Calls a particular hook. Called by CodeIgniter.php.
*
* #uses CI_Hooks::_run_hook()
*
* #param string $which Hook name
* #param array $params Extra (dynamic) parameters array
* #return bool TRUE on success or FALSE on failure
*/
public function call_hook($which = '', $params=null)
{
if ( ! $this->enabled OR ! isset($this->hooks[$which]))
{
return FALSE;
}
if (is_array($this->hooks[$which]) && ! isset($this->hooks[$which]['function']))
{
foreach ($this->hooks[$which] as $val)
{
$this->_call_single_hook($val, $params);
}
}
else
{
$this->_call_single_hook($this->hooks[$which], $params);
}
return TRUE;
}
}
In my case I extended the CI_Hooks class just overwriting the call_hook method, and the approach was appending extra parameters to the allowed "params" key of the hooks array.
After that you can invoke the hook from your controller.
Based upon your example, the code would be:
$this->hooks->call_hook('site_export_before_delete', ['filename' => $filename]);
From my point of view, this is the good solution for the problem, and it's working for me.
It sucks, but use global scoped variable.
Since the function call_hook under CI_Hook class, does not take another parameter:
public function call_hook($which = '') {
#...
}
And since I don't want to modify core files from CodeIgniter directly, I ended up using a global variable scope:
File_Where_Hook_is_Called.php:
# A variable with global scope should have an unique name,
# to avoid conflict with any other one in your entire application.
# Choose it carefully.
global $foo_hook_file_name;
$foo_hook_file_name = $file_name;
application/modules/foo/controllers/Foo.php:
public function export_site() {
global $foo_hook_file_name;
echo $foo_hook_file_name;
}
Just make sure to prefix the global var with a unique name, such as the name of the hook, to avoid conflicts with other variables.
Related
I am currently refactoring code from a page parser function to OOP.
I am having difficulties including and running code from a file into main function scope:
Object:
class phpFragment {
private $sData;
function render() {
return include $oElement->sData;
}
}
Object container class:
class pageData {
protected $aPhpFragments;
protected $aCssFragments;
public function outputData($sTag) {
switch($sTag) {
case 'php':
foreach($this->aPhpFragments as $oPhpFragment) {
return $oPhpFragment->render();
}
break;
case 'css':
foreach($this->aCssFragments as $oCssFragment) {
echo $oCssFragment->render();
}
break;
}
}
}
Main function:
function parsePage($sLanguageCode) {
$oTranslator = new translator($sLanguageCode);
$aTranslations = $oTranslator->translations('page');
$oBuilderClass = new builder($aTranslations);
//... queries to get data and set pagedata and get the template file
$oPageData = $oPage->getData();
$aTemplateTags = $oTemplate->getTags();
foreach($aTemplateTags as $sTag) {
$oPageData->outputData($sTag);
}
//....
}
Code of include (example):
<?php
$oBuilderClass->build_element(.... parameters here);
?>
I want to initiate the builder-class only once, because it contains quite some data and I don't want to recreate that on every include.
How can I return the code of the include into the parsePage function where the builderClass can be used?
You can create a Context class that will be a container of your scope variables and helps you include (execute) code inside a context. It will be a singleton class (only one instance will be created).
Here is how to use it: The method current() returns the current instance then you can export variables to the context by using the export() method, it takes a key/value array. The method execute() takes a file name as a parameter and includes it with the exported variables available, you can add temporary variables as a second parameter:
//Somewhere before execute();
oContext::current()->export([
'variable1' => 'value1',
'instance' => $instance
]);
//Then anywhere in your file:
oContext::current()->execute("toBeIncluded.php", [
'tmp_variable' => 'tmp_value'
]);
//toBeIncluded.php
echo $variable1;
echo $instance->method1();
echo $tmp_variable;
In your case:
Main function:
function parsePage($sLanguageCode) {
$oTranslator = new translator($sLanguageCode);
$aTranslations = $oTranslator->translations('page');
$oBuilderClass = new builder($aTranslations);
//export variables to your context
//Don't be aware of memroy usage objects are passed by reference
oContext::current()->export(compact('oBuilderClass'));
//... queries to get data and set pagedata and get the template file
$oPageData = $oPage->getData();
$aTemplateTags = $oTemplate->getTags();
foreach($aTemplateTags as $sTag) {
$oPageData->outputData($sTag);
}
//....
}
Object:
class phpFragment {
private $sData;
function render() {
oContext::current()->execute($oElement->sData);
}
}
You find bellow the class declaration:
oContext.class.php
/**
* Class oContext
*/
class oContext {
/**
* The singleton instance
* #var oContext
*/
private static $instance = null;
/**
* the exported variables
* #var array
*/
private $variables = [];
/**
* Return the singleton or create one if does not exist
*
* #return oContext
*/
public static function current() {
if (!self::$instance) {
self::$instance = new self;
}
return self::$instance;
}
/**
* Export an array of key/value variables
*
* #param $variables
* #return $this
*/
public function export($variables) {
foreach ($variables as $key => $value) {
$this->variables[$key] = $value;
}
return $this;
}
/**
* Include and execute a file in this context
*
* #param $file
* #param array $variables temporary exports will not be added to the context (not available in the next call)
* #return $this
*/
public function execute($file, $variables = []) {
//Populate variables
foreach (array_merge($this->variables, $variables) as $key => $value) {
${$key} = $value;
}
include $file;
return $this;
}
}
I hope this help you achieve your aim.
Good Luck.
If I correctly understand your problem then you want to execute a whole code from php file as a method called from object. If yes then you probably want to use a eval function described here.
With eval function you can read your php file as a string and evaluate it as php code instead of including it.
If your php file use a return statement then following by documentation
eval() returns NULL unless return is called in the evaluated code, in
which case the value passed to return is returned.
you can simply return that value from your method.
If your included files are as simple as you show in example then to achieve this effect you need to replace this part of your code
class phpFragment {
private $sData;
function render() {
return include $oElement->sData;
}
}
with this
class phpFragment {
private $sData;
function render() {
//read a file into variable as string
$phpCode = file_get_contents($oElement->sData);
//prepare code by adding return statement and '?>' at the begining (because you have an open tag in php files).
$phpCode = '?> ' . str_replace('$oBuilderClass->build_element', 'return $oBuilderClass->build_element', $phpCode);
//I guess that included files don't use any variables declared out of file so we need to simply escape every '$' character in file
//that they can evaluate correctly.
$phpCode = str_replace('$', '\$', $phpCode);
return eval($phpCode);
}
}
Sounds like a dependency injection problem: you want $oBuilderClass to be in scope inside the include code?
If you have access to an application dependency container, I'd register the object with that container. In generic terms, something like \Application::bind('Builder', $oBuilderClass), then later do Builder::build_element. However, that you are writing your own view renderer suggests you don't have access to a framework facility with a formal IoC container.
Supposing you don't have an IoC container, the most expedient way would be to do:
$GLOBALS['oBuilderClass'] = new builder(...);
then later in your include:
global $oBuilderClass;
$oBuilderClass->build_element(...);
This is not particularly elegant, however. You might consider passing the builder around, so that at the bottom of the call well you have:
function render(builder $oBuilderClass) {
return include $oElement->sData;
}
which puts $oBuilderClass in scope at the time of the include. I would prefer a formal IoC container first, then passing the object around, then finally if none of these work for you, then using the global variable.
I am currently working on a project involving Zend Framework 2 and Doctrine.
I am implementing a Zend\Form and using the DoctrineORMModule\Stdlib\Hydrator\DoctrineEntity to extract and hydrate data from/to the database.
In much of the tutorials I have read, it is ideal to implement the Zend\Stdlib\Hydrator\Strategy\StrategyInterface when you need to convert a specific value before hydrating an object (in my example a date time value string which is an issue when using Doctrine). However, despite my best efforts to implement this, it seems only that the extract() method in my hydration strategy is called, never the hydrate() method. EVER!
To provide a code example, this is what I am doing - I have shortened some aspects of the code for brevity;
// Service
public function getProposerForm() {
// get required classes from service manager
$proposerEntity = $this->getServiceManager()->get('tourers_entity_proposer');
$entityManager = $this->getServiceManager()->get('Doctrine\ORM\EntityManager');
$formManager = $this->getServiceManager()->get('FormElementManager');
$proposerFieldset = $formManager->get('tourers_form_proposer_fieldset');
$proposerForm = $formManager->get('tourers_form_proposal');
$proposerFieldset->setUseAsBaseFieldset(true);
$proposerForm->add($proposerFieldset);
$proposerForm->get('submit')->setValue('Continue');
$proposerForm->bind($proposerEntity);
return $proposerForm;
}
.
// Controller
public function proposerAction() {
// grab the form from the form service
$formService = $this->getServiceLocator()->get('tourers_service_forms');
$form = $formService->getProposerForm();
if (true === $this->getRequest()->isPost()) {
$form->setData($this->getRequest()->getPost());
if (true === $form->isValid()) {
$proposerEntity = $form->getData();
$encryptedPolicyId = $formService->saveProposerForm($proposerEntity, $policyId);
return $this->redirect()->toRoute('tourers/proposal/caravan',array('policyid' => $encryptedPolicyId));
} else {
$errors = $form->getMessages();
var_dump($errors);
}
}
// view
return new ViewModel(array(
'form' => $form
,'policyid' => $policyId
)
);
}
.
// Form
class ProposerFieldset extends Fieldset implements InputFilterProviderInterface, ObjectManagerAwareInterface
{
/**
* #var Doctrine\ORM\EntityManager
*/
private $objectManager;
/**
* #return Zend\Form\Fieldset
*/
public function init()
{
// set name
parent::__construct('Proposer');
// set the hydrator to the domain object
$hydrator = new DoctrineEntity($this->objectManager,true);
$hydrator->addStrategy('proposerDateOfBirth',new DateStrategy);
$this->setHydrator($hydrator);
// other form elements below here including proposerDateOfBirth
$minDate = date('dd\/mm\/yyyy',strtotime('-100 years'));
$maxDate = date('dd\/mm\/yyyy',strtotime('-16 years'));
$this->add(array(
'name' => 'proposerDateOfBirth'
,'type' => 'Zend\Form\Element\Date'
,'attributes' => array(
'class' => 'form-control'
,'id' => 'proposerDateOfBirth'
,'placeholder' => 'dd/mm/yyyy'
,'min' => $minDate
,'max' => $maxDate
,'data-date-format' => 'dd/mm/yyyy'
),
'options' => array(
'label' => 'Date of Birth',
)
));
}
}
.
// Hydrator Strategy
namespace Tourers\Hydrator\Strategy;
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
class DateStrategy implements StrategyInterface {
/**
* (non-PHPdoc)
* #see Zend\Stdlib\Hydrator\Strategy.StrategyInterface::extract()
*/
public function extract($value) {
var_dump($value . ' extracted'); // GETS CALLED
return $value;
}
/**
* (non-PHPdoc)
* #see Zend\Stdlib\Hydrator\Strategy.StrategyInterface::hydrate()
*/
public function hydrate($value) {
var_dump($value . ' hydrated'); // NEVER CALLED
return $value;
}
}
I also discovered this behaviour. As yet I am still lost as to why my custom strategy was not being called.
The Strategies are applied inside the hydrateValue() and extractValue() functions, so its necessary for these functions to be called by the hydrate() and extract() functions in order for Strategies or Custom Strategies to be applied.
My problem was evident in the hydrateByReference() function inside DoctrineModule\Stdlib\Hydrator\DoctrineObject.
It seems that $this->hydrateValue(...) is only called the field has "associations". I do not yet know what these "associations" are, or what I am doing wrong for them to not exist in my data.
When I compared it to the extractByReference() function, I noticed that it always calls $this->extractValue() and does not require any "associations".
In my application, I had already implemented a custom Hydrator class. This means that when I create a form, the Hydrator and Strategies are automatically applied.
Inside my Form's init() function, I assign the custom Hydrator.
Inside my Custom Hydrator's __construct(), I add the strategies, and Custom Strategies.
So all I needed to do was to override hydrateByReference(...) in my Custom Hydrator to solve the problem. The example is below.
NOTES:
You may also need to override hydrateByValue(...) if you use "by value" hydration.
My solution may break the functionality of "associations".
My Custom Hydrator class:
class maintenance
extends DoctrineHydrator
{
/**
* Constructor
*
* #param ObjectManager $objectManager The ObjectManager to use
*/
public function __construct($objectManager)
{
/*
* Just call the parent class.
*/
parent::__construct($objectManager,
'Maintenance\Entity\maintenance', // The FQCN of the hydrated/extracted object
false // If set to true, hydrator will always use entity's public API
);
/*
* Now set up our strategies, and attach them
* to the appropriate fields.
*/
$this->addStrategy('purchasedate', new TIGDateStrategy());
$this->addStrategy('maintexpirydate', new TIGDateStrategy());
}
/**
* SF Modification, to ensure we call this->hydrateValue on
* all values before doing anything else with the data.
* This way, we convert the data first, before trying to
* store it in a DoctrineEntity.
*
* Hydrate the object using a by-reference logic (this means that values are modified directly without
* using the public API, in this case setters, and hence override any logic that could be done in those
* setters)
*
* #param array $data
* #param object $object
* #return object
*/
protected function hydrateByReference(array $data, $object)
{
$object = $this->tryConvertArrayToObject($data, $object);
$metadata = $this->metadata;
$refl = $metadata->getReflectionClass();
foreach ($data as $field => $value) {
// Ignore unknown fields
if (!$refl->hasProperty($field)) {
continue;
}
// SF Mod
$value = $this->hydrateValue($field, $value);
// End SF Mod
$value = $this->handleTypeConversions($value, $metadata->getTypeOfField($field));
$reflProperty = $refl->getProperty($field);
$reflProperty->setAccessible(true);
if ($metadata->hasAssociation($field)) {
$target = $metadata->getAssociationTargetClass($field);
if ($metadata->isSingleValuedAssociation($field)) {
$value = $this->toOne($target, $this->hydrateValue($field, $value));
$reflProperty->setValue($object, $value);
} elseif ($metadata->isCollectionValuedAssociation($field)) {
$this->toMany($object, $field, $target, $value);
}
} else {
$reflProperty->setValue($object, $value);
}
}
return $object;
}
}
Seems like this is a very common problem for beginners with CodeIgniter, but none of the solutions I've found so far seems very relevant to my problem. Like the topic says I'm trying to include a custom class in CodeIgniter.
I'm trying to create several objects of the class below and place them in an array, thus I need the class to be available to the model.
I've tried using the load (library->load('myclass') functions within CodeIgniter which sort of works, except it tries to create an object of the class outside the model first. This is obviously a problem since the constructor expects several parameters.
The solutions I've found so far is
A simple php include which seems fine enough, but since I'm new to
CodeIgniter I want to make sure I'm sticking to it as much as
possible.
Creating a "wrapper class" as suggested here, however I'm uncertain how I would implement this.
The class I want to include,
User.php
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class User{
public $ID = 0;
public $username = 0;
public $access_lvl = 0;
public $staff_type = 0;
public $name = 0;
public function __construct($ID, $username, $access_lvl, $staff_type, $name)
{
$this->ID = $ID;
$this->username = $username;
$this->access_lvl = $access_lvl;
$this->staff_type = $staff_type;
$this->name = $name;
}
public function __toString()
{
return $this->username;
}
}
?>
Method (Model) which needs the User.php
function get_all_users()
{
$query = $this->db->get('tt_login');
$arr = array();
foreach ($query->result_array() as $row)
{
$arr[] = new User
(
$row['login_ID'],
$row['login_user'],
$row['login_super'],
$row['crew_type'],
$row['login_name']
);
}
return $arr;
}
And finally the controller,
function index()
{
$this->load->library('user');
$this->load->model('admin/usersmodel', '', true);
// Page title
$data['title'] = "Some title";
// Heading
$data['heading'] = "Some heading";
// Data (users)
$data['users'] = $this->usersmodel->get_all_users();
If you have PHP version >= 5.3 you could take use of namespaces and autoloading features.
A simple autoloader library in the library folder.
<?php
class CustomAutoloader{
public function __construct(){
spl_autoload_register(array($this, 'loader'));
}
public function loader($className){
if (substr($className, 0, 6) == 'models')
require APPPATH . str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php';
}
}
?>
The User object in the model dir. ( models/User.php )
<?php
namespace models; // set namespace
if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class User{
...
}
And instead of new User... new models\User ( ... )
function get_all_users(){
....
$arr[] = new models\User(
$row['login_ID'],
$row['login_user'],
$row['login_super'],
$row['crew_type'],
$row['login_name']
);
...
}
And in controller just make sure to call the customautoloader like this:
function index()
{
$this->load->library('customautoloader');
$this->load->model('admin/usersmodel', '', true);
// Page title
$data['title'] = "Some title";
// Heading
$data['heading'] = "Some heading";
// Data (users)
$data['users'] = $this->usersmodel->get_all_users();
CodeIgniter doesn't really support real Objects.
All the libraries, models and such, are like Singletons.
There are 2 ways to go, without changing the CodeIgniter structure.
Just include the file which contains the class, and generate it.
Use the load->library or load_class() method, and just create new objects. The downside of this, is that it will always generate 1 extra object, that you just don't need. But eventually the load methods will also include the file.
Another possibility, which will require some extra work, is to make a User_Factory library.
You can then just add the object on the bottom of the file, and create new instances of it from the factory.
I'm a big fan of the Factory pattern myself, but it's a decision you have to make yourself.
I hope this helped you, if you have any questions that are more related to the implementation, just let me/us know.
Including a class file is not a bad approach.
In our projects, we do the same, add an another layer to MVC, and thats a Service Layer which the Controllers calls and Service calls the Model. We introduced this layer to add Business Logic seperate.
So far, we have been using it, and our product has gone large too, and still we find no difficulty with the decision of including files that we had made in the past.
Codeigniter has a common function to instantiate individual classes.
It is called load_class(), found in /system/core/Common.php
The function;
/**
* Class registry
*
* This function acts as a singleton. If the requested class does not
* exist it is instantiated and set to a static variable. If it has
* previously been instantiated the variable is returned.
*
* #access public
* #param string the class name being requested
* #param string the directory where the class should be found
* #param string the class name prefix
* #return object
*/
The signature is
load_class($class, $directory = 'libraries', $prefix = 'CI_')
An example of it being used is when you call the show_404() function.
After a brief google search, I was inspired to make my own autoloader class. It's a bit of a hack, since I use custom Codeigniter library to preform auto-loading, but for me this is the best way, that I'm aware of, of loading all the classes, I require, without compromising my application architecture philosophy, to fit it into Codeigniter way of doing things. Some might argue that Codeigniter is not the right framework for me and that might be true, but I'm trying things out and playing around with various frameworks and while working on CI, I came up with this solution.
1. Auto-load new custom library by editing applicaion/config/autoload.php to include:
$autoload['libraries'] = array('my_loader');
and any other libraries you might need.
2. Then add library class My_loader. This class will be loaded on every request and when its constructor is run, it will recursively search through all sub-folders and require_once all .php files inside application/service & application/models/dto folders. Warning: folders should not have dot in the name, otherwise function will fail
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class My_loader {
protected static $_packages = array(
'service',
'models/dto'
);
/**
* Constructor loads service & dto classes
*
* #return void
*/
public function __construct($packages = array('service', 'models/dto'))
{
// files to be required
$toBeRequired = array();
// itrate through packages
foreach ($packages as $package) {
$path = realpath(APPPATH . '/' . $package . '/');
$toBeRequired = array_merge($toBeRequired, $this->findAllPhpFiles($path));
}
/**
* Require all files
*/
foreach ($toBeRequired as $class) {
require_once $class;
}
}
/**
* Find all files in the folder
*
* #param string $package
* #return string[]
*/
public function findAllPhpFiles($path)
{
$filesArray = array();
// find everithing in the folder
$all = scandir($path);
// get all the folders
$folders = array_filter($all, get_called_class() . '::_folderFilter');
// get all the files
$files = array_filter($all, get_called_class() . '::_limitationFilter');
// assemble paths to the files
foreach ($files as $file) {
$filesArray[] = $path . '/' . $file;
}
// recursively go through all the sub-folders
foreach ($folders as $folder) {
$filesArray = array_merge($filesArray, $this->findAllPhpFiles($path . '/' . $folder));
}
return $filesArray;
}
/**
* Callback function used to filter out array members containing unwanted text
*
* #param string $string
* #return boolean
*/
protected static function _folderFilter($member) {
$unwantedString = '.';
return strpos($member, $unwantedString) === false;
}
/**
* Callback function used to filter out array members not containing wanted text
*
* #param string $string
* #return boolean
*/
protected static function _limitationFilter($member) {
$wantedString = '.php';
return strpos($member, $wantedString) !== false;
}
}
After 18 hours I managed to include a library in my control without initialisation (the constructor was the problem, because of that and i could't use the standard codeiginiter $this->load->library() ).
Follow the https://stackoverflow.com/a/21858556/4701133 . Be aware for further native class initialization use $date = new \DateTime()with back-slash in front otherwise the function will generate an error !
I have an helper class with some static functions. All the functions in the class require a ‘heavy’ initialization function to run once (as if it were a constructor).
Is there a good practice for achieving this?
The only thing I thought of was calling an init function, and breaking its flow if it has already run once (using a static $initialized var). The problem is that I need to call it on every one of the class’s functions.
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: 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.)
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)();
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.
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');
?>
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.
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 )
I am trying to figure out how I want to handle settings in my PHP app. I have pretty much decide that I would like to use a Confg class file so it will be autoloaded and flexible in the future. Below is some stuff I was playing with.
I know you cannot set a variable to popluate a Constant so I then try to use a public static property.
Why can I not set public static $ip = $_SERVER['REMOTE_ADDR']; ??
<?php
// config.class.php
class Config
{
const URL = 'http://www.foo.com';
const DB_User = 'dbname';
public static $test = 'test string';
public static $ip = $_SERVER['REMOTE_ADDR'];
}
///////////////////////////////////////////////////////
//index.php
// works
echo Config::URL;
// works
echo Config::$test;
// DOES NOT WORK
echo Config::$ip;
?>
members must be initialized with a constant expression (like a string constant, numeric literal, etc). php will give a parse error if you try to initialize a member with a dynamic expression (like the value of a variable or a function call)...
this is unlike some other langs like python, or javascript which consider class definitions to be on par with executable expressions... so your idea here is very functional, but php doesn't support it now at least...
but there are some alternatives to deal with this:
add the initialization after the class definition:
class C {...}
C::$var = $_SERVER['REMOTE_ADDR'];
or have an init function:
function init()
{
if (self::$init === false) {
self::$var = $_SERVER['REMOTE_ADDR'];
self::$init = true;
}
}
C::init();
or with newer php you can use the __autoload() hook to do some static initializations...
Although this does not answer your question (jspcal answers it correctly), a quick solution that might fit your needs would to use the Singleton design pattern. Here is an alternative:
<?php
// config.class.php
class Config
{
/**
* Instance container
* #var Config
*/
private static $instance = null;
/**
* Constants container
* #var array
*/
private $constants = array(
'url' => 'http://www.foo.com/',
'db_user' => 'dbname'
);
/**
* Options container
* #var array
*/
private $options = array();
/**
* Don't allow outside initialization
*/
private function __construct()
{
// Set options (could load from external source)
$this->options = array(
'test' => 'test string',
'ip' => $_SERVER['REMOTE_ADDR']
);
}
/**
* Disable cloning
*/
private function __clone() { }
/**
* Get new instance of class
*/
public function getInstance()
{
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Retrieve value with constants being a higher priority
* #param $key Key to get
*/
public function __get( $key )
{
if ( isset( $this->constants[$key] ) ) {
return $this->constants[$key];
} elseif ( isset( $this->options[$key] ) ) {
return $this->options[$key];
}
}
/**
* Set a new or update a key / value pair
* #param $key Key to set
* #param $value Value to set
*/
public function __set( $key, $value )
{
$this->options[$key] = $value;
}
}
///////////////////////////////////////////////////////
//index.php
// works
$config = Config::getInstance();
echo $config->url;
echo $config->test;
echo $config->ip;
Updated: Not sure if you want the constants / options with that kind of priority. It's just an example.
try to use define() to do that (give a constant!):
// config.class.php
define(REMOTE_ADDR, $_SERVER['REMOTE_ADDR']);
class Config
{
const URL = 'http://www.foo.com';
const DB_User = 'dbname';
public static $test = 'test string';
public static $ip = REMOTE_ADDR;
}
Not a direct answer to your question, but why don't you use a less hardcoded approach, e.g. a generic Config class you can reuse in your apps
// MyConfig.php
class MyConfig {
protected $_data;
public function __construct($path)
{
$config = include $path;
$this->_data = $config;
}
public function __get($val)
{
if(array_key_exists($val, $this->_data)) {
return $this->_data['val'];
} else {
trigger_error("Key $val does not exist", E_USER_NOTICE);
}
}
}
that you can fill from an array for a specific app
// app-config.php
return array(
'ip' => $_SERVER['REMOTE_ADDR'],
'url' => 'http://www.foo.com';
'db' => array(
'host' => 'foo.com',
'port' => 3306
),
'caching' => array(
'enabled' => false
)
);
and then use in your bootstrap like
$config = new MyConfig('/path/to/app-config.php');
This is not answering your question, but in my opinion, a better way to deal with configurations is actually to use a real configuration file, like an INI or XML file.
You could use e.g. the Zend Config class to read and write such files (and this class can even deal with a plain PHP array as configuration.
In the end this will make your code easier to maintain.
After reading other answers and comments you might also be interested in the Zend Registry class.
In general I would advice to use a framework or readymade components for such stuff. You don't need to reinvent the wheel and you can profit from the other's experience.
It won't work because PHP just doesn't allow it.
Generally I wouldn't suggest putting your app's config into a class (at least not in PHP), there are no real advantages in my opinion - better put it into a global array or so :)
PHP simply doesn't allow that. If you think about it, it doesn't make sense to have the REMOTE_ADDR as part of your configuration data anyway. You can consider it to be part of the request data, and not the application configuration.
As for your configuration, I would suggest using an .ini file for your configuration because PHP has a built-in INI parser (see the parse_ini_file function). This would be consistent with PHP's choice of INI file format for its own configuration (i.e., php.ini).
Check out this question, for example: What's Is the Best File Format for Configuration Files
The simple reason why you cannot assign $_SERVER['REMOTE_ADDR'] to the class attribute is this.
In PHP, Class attribute value can only be a data type and not a variable, and since $_SERVER is a global array variable, it can't be assigned to an attribute.