在不改变源代码的情况下扩展程序。 SOLID PHP OOP

Let's say we have few elements:

1) Simple PHP Class

<?php 
  class page {
    function display() {
      // Display widgets implementing widget interface
    }
 }
?>

2) Interface for Widget Classes

<?php 
  interface Widget {
    function print();
  }
?>

3) And few classes implementing the Widget interface

<?php
  class Poll implements Widget {
    function print() {
      echo "I'm a poll";
    }
  }

 class Form implements Widget {
   function print() {
     echo "I'm a form";
   }
 } 
?>

What is the right way taking in mind SOLID PHP OOP logic to automatically run print() function in all classes implementing Widget interface? Let's assume all the files are already included.

Basically, whenever new class implementing Widget is created, it's response should be printed out in a page.

Maybe all my logic behind this is wrong?

The only way I could think of without using any config or database is to get all declared classes and check the ones that implement the Widget interface:

This is the modified Page class

class Page {

    public $widgets = [];

    public function addWidget(Widget $widget)
    {
        $this->widgets[] = $widget;
    }

    public function display()
    {
        // Now you have an array of Widgets you can loop through them
        // And run print()
        foreach ($this->widgets as $widget) {
            $widget->print();
        }
    }
}

And then we can get all classes (that are loaded) that implement the Widget interface:

$widgets = array_filter(
    get_declared_classes(),
    function ($className) {
        return in_array('Widget', class_implements($className));
    }
);

And then we pass them to Page:

$page = new Page();
foreach ($widgets as $item) {
    $widget = new $item;
    $page->addWidget($widget);
}

// Now Page has all Widgets we can call display()
$page->display();

But be aware that get_declared_classes() will return a lot of classes (160 in my PC), so we are looping through them all and checking if each one implements the Widget interface.

Now whenever you have a class that implements the Widget interface it will be used by Page. To be honest I don't know if there is a better way so it might be worth it to wait for other answers.

You can always use a little bit of magic.

Let's say you have all widgets in one directory. You can fetch them with some filesystem operations, iterate over the results, verify whether it is an instance of Widet, create new instance and run it.

This solution has one big benefit. You write this code once and each new widget will be automatically included and run. This solves the open close principle :)

But it also has some cons:

  • you have to use filesystem,
  • IDE will does not know that the Widget implementation are used,
  • it is harder to understand.

Another solution is to have a class like WidgetCollection which will know all the widget in the system and it can provide you them. So when you will create a new widget implementation you will have to add it to the collection, that's all :)

Cheers.

I can't really say if it adheres to SOLID principles but I can think of two approaches:

  1. Store widgets in a directory and use PSR-4 autoloading. Then use file system functions to scan the directory and create instances of class names that correspond to file names. (Make sure you have full control on directory contents or you'll be hacked!)

    // Totally untested code with no error checking
    foreach (glob(YOUR_BASE_DIR_HERE . '/Widget/*.php') as $file) {
        $className = 'Widget\\' . basename($file, '.php');
        $widget = new $className();
        $widget->print();
    }
    

    This assumes that widgets do not need to be configured thus instance creation can be automated.

  2. Tweak the widget spec so that each file returns a properly initialised instance and include it (same warnings apply):

    foreach (glob(YOUR_WIDGETS_DIR_HERE . '/*.php') as $file) {
        $widget = include $file;
        $widget->print();
    }
    

    … where every included file ends with something like return new Poll();.

    It's important that included files do not pollute the global scope with variables. Depending on how complex widgets are expected to be, you may want to have actual class definitions elsewhere so you adhere to PSR-1 coding standard: Files SHOULD either declare symbols (classes, functions, constants, etc.) or cause side-effects (e.g. generate output, change .ini settings, etc.) but SHOULD NOT do both.