2

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?

PIDZB
  • 903
  • 1
  • 15
  • 38
  • you mention an include in the `render()` function at the top of the page is this the contents that you display at the bottom of the page? Is this the same include. The `$oBuilderClass` is generated in the `parsePage` function but referenced in the include so it looks quite confused to me..... – Martin Jul 01 '16 at 15:44
  • "How can I return the code of the include into the parsePage function" which include? Do you mean you only wish to instantiate the class `builder` once, ie. the [singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern), or that you only want to [`include`](http://php.net/manual/en/function.include.php) the source file containing that class once? – Schlaus Jul 02 '16 at 22:46

3 Answers3

3

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.

Ismail RBOUH
  • 10,292
  • 2
  • 24
  • 36
0

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);
    }
}
zajonc
  • 1,935
  • 5
  • 20
  • 25
  • Thank you for your solution. I have read, however, that eval() is a dangerous function and to include this in my core code, seems bad design, worse than mine atm XD – PIDZB Jul 05 '16 at 11:53
  • Yes, you are right that in general it can be dangerous, but if you expect only simple code for object building and if you can describe this code with a pattern (and validate it with this pattern) then you can reduce the risk. The advantage of the eval function is that it's able to return a value from evaluated code like a function (this is a reason why I proposed my answer). – zajonc Jul 05 '16 at 14:22
0

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.

Community
  • 1
  • 1
bishop
  • 37,830
  • 11
  • 104
  • 139
  • Thank you for your explanation/idea, but I don't want to use a global variable, its bad code – PIDZB Jul 05 '16 at 11:52