How to generate or modify a PHP class at runtime?
Asked Answered
R

5

2

The schmittjoh/cg-library seems what I need, but there is no documentation at all.

This library provides some tools that you commonly need for generating PHP code. One of it's strength lies in the enhancement of existing classes with behaviors.

Given A class:

class A {}

I'd like to modify, at runtime of course and with some cache mechanism, class A, making it implementing a given interface:

interface I
{
    public function mustImplement();
}

... with a "default" implementation for method mustImplement() in A class.

Roguery answered 29/9, 2012 at 8:28 Comment(3)
What is your particular requirement, often if you explain your problem people can suggest alternative solutionsBourassa
Bad idea, code generation during runtime.Eustace
Are you looking for Lazy Loading? Please add more context as there is several ways to achieve this with regular Design Patterns but which one depends on why and what for you need this.Imprison
O
5

Note: OP needs PHP 5.3 (it was not tagged that way before), this question is a general outline for PHP 5.4.

You can do that with defining the interface and adding traits that contain the default implementation for those interfaces.

Then you create a new class definition that

  • extends from your base-class,
  • implements that interface and
  • uses the default traits.

For an example, see Traits in PHP – any real world examples/best practices?

You can easily generate that class definition code and either store it and include it or eval it straight ahead.

If you make the new classname containing all the information it consists of (in your case the base classname and the interface), you can prevent to create duplicate class definitions easily.

This works without any PHP extension like runkit. If you bring serialize into the play, you can even overload existing objects at runtime with the new interface in case they can serialize / deserialize.

You can find a code-example that has been implementing this in:

Offutt answered 29/9, 2012 at 8:31 Comment(5)
Yes, this requires PHP 5.4 because of the traits, but it works. You could emulate traits as well by injecting the function definitions at runtime and store them in classes before. That is then more work.Offutt
I upvoted your answer but can't accept it. I can't move to 5.4 yet.Roguery
Well, you can create an adoption to this for PHP < 5.4 by emulating traits. Traits in PHP are copy and paste on the compiler level, so you could store the default implementation inside a class of it's own and copy over the class body injecting it into the new class. For overwriting, you need to create your own mechanism then (duplicate functions etc), use reflection to control that.Offutt
In that case, what is bad about the CG library? I have not used it, but it looks like that it offers some support for these actions. Also read again the comment by Toby Allen, there is much truth in it.Offutt
Ok, now I have to think better on what I'm trying to do. Your solution seems fine and so your link about traits.Roguery
I
6

You can also use a Role Object pattern and good old aggregation.

Instead of having smart Entities that contain all your business logic, you make them dumb and move all the business logic into Roles that aggregate the dumb Entities then. Your behaviors are then first-class citizens living inside the Roles.

Example:

interface BannableUser
{
    public function ban();
}

Having an interface with one particular behavior follows the Interface Segregation Principle. It also dramatically increases possible reuse since you are more likely to reuse individual behaviors than an Entity with an application-specific collection of behaviors.

Now to implement that, you create an appropriate Role Class:

class BannableUserRole implements BannableUser
{
     private $user;

     public function __construct(User $user)
     {
         $this->user = $user;
     }

     public function ban()
     {
         $this->user->isBanned = true;
     }
}

You still have a User entity but it's completely stripped of all behaviors. It's essentially just a bag of Getters and Setters or public properties. It represents what your System is. It's the data part, not the interaction part. The interaction is inside the Roles now.

class User
{
    public $isBanned;

    // … more properties
}

Now assuming you have some sort of Web UI, you can do the following in your controller:

class BanUserController implements RequestHandler
{
    // …

    public function handleRequest(Request $request)
    {
        $userId = $request->getVar('user_id');
        $user = $this->userRepository->findById($userId);
        $bannableUser = new BannableUserRole($user);
        $bannableUser->ban();
    }
}

You can decouple this further by moving the actual lookup of the User and assignment of the Role into a UseCase class. Let's call it Context:

class BanUserContext implements Context
{
    public function run($userId)
    {
        $user = $this->userRepository->findById($userId);
        $bannableUser = new BannableUserRole($user);
        $bannableUser->ban();
    }
}

Now you have all the business logic inside your Model layer and fully isolated from your User Interface. The Contexts are what your system does. Your Controller will only delegate to the appropriate Context:

class BanUserController implements RequestHandler
{
    // …

    public function handleRequest(Request $request)
    {
        $this->banUserContext->run($request->getVar('user_id'));

    }
}

And that's it. No need for Runkit or similar hackery. The above is a simplified version of the Data Context Interaction architectural pattern, in case you want to further research this.

Imprison answered 30/9, 2012 at 10:5 Comment(1)
Traits offer default implementation for interfaces.Edwardoedwards
O
5

Note: OP needs PHP 5.3 (it was not tagged that way before), this question is a general outline for PHP 5.4.

You can do that with defining the interface and adding traits that contain the default implementation for those interfaces.

Then you create a new class definition that

  • extends from your base-class,
  • implements that interface and
  • uses the default traits.

For an example, see Traits in PHP – any real world examples/best practices?

You can easily generate that class definition code and either store it and include it or eval it straight ahead.

If you make the new classname containing all the information it consists of (in your case the base classname and the interface), you can prevent to create duplicate class definitions easily.

This works without any PHP extension like runkit. If you bring serialize into the play, you can even overload existing objects at runtime with the new interface in case they can serialize / deserialize.

You can find a code-example that has been implementing this in:

Offutt answered 29/9, 2012 at 8:31 Comment(5)
Yes, this requires PHP 5.4 because of the traits, but it works. You could emulate traits as well by injecting the function definitions at runtime and store them in classes before. That is then more work.Offutt
I upvoted your answer but can't accept it. I can't move to 5.4 yet.Roguery
Well, you can create an adoption to this for PHP < 5.4 by emulating traits. Traits in PHP are copy and paste on the compiler level, so you could store the default implementation inside a class of it's own and copy over the class body injecting it into the new class. For overwriting, you need to create your own mechanism then (duplicate functions etc), use reflection to control that.Offutt
In that case, what is bad about the CG library? I have not used it, but it looks like that it offers some support for these actions. Also read again the comment by Toby Allen, there is much truth in it.Offutt
Ok, now I have to think better on what I'm trying to do. Your solution seems fine and so your link about traits.Roguery
E
2

I needed a tool to edit PHP classes (specifically Doctrine Entities), I could not find it so I created one that might help you, I documented it heavily. So if you want to give it a try, I'll happily help.

Here it is, SourceEditor

It gives you an API like this:

    /* @var $classEditor DocDigital\Lib\SourceEditor\PhpClassEditor */
    $classEditor->parseFile($classPath);
    $classEditor->getClass('a')->getMethod('b')->addAnnotation('@auth Juan Manuel Fernandez <[email protected]>');
    $classEditor->getClass('a')->getAttribute('attr')->addAnnotation('@Assert\Choice(...)');
    $classEditor->getClass('a')->addAttribute($attr2);
    $classEditor->getClass('a')->addUse('use DocDigital\Bundle\DocumentBundle\DocumentGenerator\Annotation as DdMapping;');
    $classEditor->getClass('a')->addConst('    const CONSTANT = 1;');

    file_put_contents($classPath, $classEditor->getClass('a')->render(false));
Edwardoedwards answered 11/4, 2014 at 0:17 Comment(0)
M
0

Runkit extension can help you

Runkit_Sandbox — Runkit Sandbox Class -- PHP Virtual Machine
Runkit_Sandbox_Parent — Runkit Anti-Sandbox Class
runkit_class_adopt — Convert a base class to an inherited class, add ancestral methods when appropriate
runkit_class_emancipate — Convert an inherited class to a base class, removes any method whose scope is ancestral
runkit_constant_add — Similar to define(), but allows defining in class definitions as well
runkit_constant_redefine — Redefine an already defined constant
runkit_constant_remove — Remove/Delete an already defined constant
runkit_function_add — Add a new function, similar to create_function
runkit_function_copy — Copy a function to a new function name
runkit_function_redefine — Replace a function definition with a new implementation
runkit_function_remove — Remove a function definition
runkit_function_rename — Change the name of a function
runkit_import — Process a PHP file importing function and class definitions, overwriting where appropriate
runkit_lint_file — Check the PHP syntax of the specified file
runkit_lint — Check the PHP syntax of the specified php code
runkit_method_add — Dynamically adds a new method to a given class
runkit_method_copy — Copies a method from class to another
runkit_method_redefine — Dynamically changes the code of the given method
runkit_method_remove — Dynamically removes the given method
runkit_method_rename — Dynamically changes the name of the given method
runkit_return_value_used — Determines if the current functions return value will be used
runkit_sandbox_output_handler — Specify a function to capture and/or process output from a runkit sandbox
runkit_superglobals — Return numerically indexed array of registered superglobals
Maus answered 29/9, 2012 at 8:31 Comment(1)
this is more a comment than an answer.Offutt
Y
0

You can look: https://github.com/ptrofimov/jslikeobject

Author implemented dynamic JS-like objects with support of inheritance.

But it is more like a joke than real proposition.

Ytterbite answered 8/1, 2013 at 18:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.