The name is quite bad, but I really don't know what else to call it.
I'm trying to make a extendable and modular plugin system for my website. I need to be able to access plugin php files that exist in a plugin directory and get access to their classes to call functions such as getting the html content that the plugin should show and more.
Below is a semi-pseudo code example of what I am trying to achieve, but how to actually arbitrarily load the plugins is where I am stuck (PluginLoader.php).
-Max
//BasePlugin.php
abstract class BasePlugin
{
public function displayContent()
{
print "<p>Base Plugin</p>";
}
};
//ExamplePlugin.php -> In specific plugin directory.
require('../BasePlugin.php');
class ExamplePlugin extends BasePlugin
{
public static function Instance()
{
static $inst = null;
if ($inst === null) {
$inst = new ExamplePlugin();
}
return $inst;
}
public function displayContent()
{
print "<p>Example Plugin</p>";
}
}
//PluginLoader.php
foreach($pluginFile : PluginFilesInDirectory) { // Iterate over plugin php files in plugin directory
$plugin = GetPlugin($pluginFile); // Somehow get instance of plugin.
echo plugin->displayContent();
}
I'm guessing here, but it seems to me that you need to:
get a list of the plugins in the desired directory.
include or require the plugin's class file.
create an instance of the class.
call the plugin's displayContent() method.
So, you probably want to do something like
$pluginDir = 'your/plugin/directory/' ;
$plugins = glob($pluginDir . '*.php') ;
foreach($plugins as $plugin) {
// include the plugin file
include_once($plugin) ;
// grab the class name from the plugin's file name
// this finds the last occurrence of a '/' and gets the file name without the .php
$className = substr($plugin,strrpos($plugin,'/') + 1, -4) ;
// create the instance and display your test
$aPlugin = $className::Instance() ;
$aPlugin->displayContent() ;
}
There's probably a cleaner way to do it, but that will ready your directory, get the plugins' code, and instantiate each one. How you manage/reference them afterwards depends on how your plugins register with your application.
Related
The Sonata Media Bundle you have the thumbnail property on a provider in the config where you can specify either
sonata.media.thumbnail.format
sonata.media.thumbnail.liip_imagine
This all fine and the sonata.media.thumbnail.format one works fine for everything I want to achieve. My problem comes in with what happens within these files.
In the FormatThumbnail.php there is a function called generatePublicUrl where they generate the url of the media file and also the name of the formatted file. They use the media id within the name or url. If you have private files not everyone must be able to see this causes a problem with it is easy to manipulate the id to another id.
I know the public files that will be served will always stay public so if the url can be guessed the user will access the file. For this specific reason I want to try and replace that id with the unique reference that the bundle uses before they create the actual formatted files as this will not be as easy to just change.
I am aware that there are still risks of leaking out data.
I want to change this
public function generatePublicUrl(MediaProviderInterface $provider, MediaInterface $media, $format)
{
if ($format == 'reference') {
$path = $provider->getReferenceImage($media);
} else {
$path = sprintf('%s/thumb_%s_%s.%s', $provider->generatePath($media), $media->getId(), $format, $this->getExtension($media));
}
return $path;
}
to this
public function generatePublicUrl(MediaProviderInterface $provider, MediaInterface $media, $format)
{
if ($format == 'reference') {
$path = $provider->getReferenceImage($media);
} else {
$path = sprintf('%s/thumb_%s_%s.%s', $provider->generatePath($media), $media->getProviderReference(), $format, $this->getExtension($media));
}
return $path;
}
How do I override the file that the bundle just picks up the change?
I have followed the steps on Sonata's site on how to install and set up the bundle using the easy extends bundle. I have my own Application\Sonata\MediaBundle folder that is extending the original Sonata\MediaBundle.
For installation related information have a look through the documentation(https://sonata-project.org/bundles/media/master/doc/reference/installation.html)
However I tried to create my own Thumbnail folder and creating a new FormatThumbnail.php as follows
<?php
namespace Application\Sonata\MediaBundle\Thumbnail;
use Sonata\MediaBundle\Model\MediaInterface;
use Sonata\MediaBundle\Provider\MediaProviderInterface;
use Sonata\MediaBundle\Thumbnail\FormatThumbnail as BaseFormatThumbnail;
class FormatThumbnail extends BaseFormatThumbnail
{
/**
* Overriding this to replace the id with the reference
*
* {#inheritdoc}
*/
public function generatePublicUrl(MediaProviderInterface $provider, MediaInterface $media, $format)
{
if ($format == 'reference') {
$path = $provider->getReferenceImage($media);
} else {
$path = sprintf('%s/thumb_%s_%s.%s', $provider->generatePath($media), $media->getProviderReference(), $format, $this->getExtension($media));
}
return $path;
}
}
But the bundle still generates everything using the id instead of the reference. Is there a more specific way to achieve extending this file and overriding the function?
After looking at a few different bundles and after looking in code I found that they physically have a parameter which is set to use Sonata\MediaBundle\Thumbnail\FormatThumbnail.
The solution is to override the parameter in the config aswell.
#As top level classification in app/config/config.yml
parameters:
sonata.media.thumbnail.format: Application\Sonata\MediaBundle\Thumbnail\FormatThumbnail
This way the custom FormatThumbnail class is injected everywhere it will be used within the bundle.
I have a custom content type called "program" that I am trying to load via a drupal module.
The .module file includes a class called Program that has a method called
getAllPrograms() using include_once(drupal_get_path('module', 'progs') . '/progs.php');
When i try and load nodes using either node_load() or node_load_multiple() i get one of two different errors randomly.
either:
Fatal error: Fatal error: Call to undefined function user_access() in /mypath/modules/filter/filter.module on line 1035
or
Error: Call to undefined function token_get_entity_mapping() in /mypath//sites/all/modules/contrib/token/token.tokens.inc, line 767
Note: 99% of times it is the first error, and occasionally i would recieve the token_get_entity error.
The strange thing is, while i have been trying different things to resolve the error I have been able to get both of these functions to work for a period but as soon as i clear the Drupal Cache i get the error again.
What I have tried
Disabling and enabling the user module via the database.
Checking the paths and status are correct for the main modules (system, user, block etc)
using db_select to get a list of node ids and then use node_load() (with a loop) and node_load_multiple() to load the nodes. This is one of the things that started working for a short time until i cleared the cache.
Tested to see if i can call user_access() from my .module file. This does not work and returns the same call to undefined function error.
Here is the code that I have (not progs an anonymized name)
progs.module
include_once(drupal_get_path('module', 'progs') . '/progs.php');
progs.php
if( !class_exists('progs') ):
class progs
{
//a bunch of properties
function __construct()
{
// load partial includes and objects
$this->load_partial_inclues();
//retrieve all programs that are open
$this->open_programs = Program::getAllOpenPrograms();
}
function load_partial_inclues()
{
//includes
include_once(drupal_get_path('module', 'progs') . '/core/objects/program.php');
}
}
function progs()
{
global $progs;
if( !isset($progs) )
{
$progs = new progs();
}
return $progs;
}
// initialize
progs();
endif;
Note: I load the $progs into the global space so i can call it elsewhere in my module.
program.php
if( !class_exists('Program') ):
class Program
{
//a bunch of properties
public static function getAllOpenPrograms()
{
// This is the line that causes all of the issues.
$result = node_load_multiple('',array('type' => 'program'));
dpm($result);
}
Thanks in advance!
Like Mike Vranckx mentioned, if you call progs() directly when you include it in progs.module, Drupal basically hasn't bootstrapped, i.e. hasn't started running fully yet. Suggest you put your progs() in progs_init() or similar so that Drupal will invoke it at the right time.
Here's a proposed way that follows your initial structure quite closely, and below you will see an alternative that better follows Drupal's conventions.
New progs.module
/**
* Implements hook_init().
*/
function progs_init(){
progs();
}
And modify your progs.php
// Why are you doing this check? Are you defining this class elsewhere in your project? If not you can safely ignore this
//if( !class_exists('progs') ):
// Convention is to name classes with Pascal case btw.
class progs
{
//a bunch of properties
function __construct()
{
// load partial includes and objects
$this->load_partial_inclues();
//retrieve all programs that are open
$this->open_programs = Program::getAllOpenPrograms();
}
function load_partial_inclues()
{
//includes
include_once(drupal_get_path('module', 'progs') . '/core/objects/program.php');
}
}
function progs()
{
global $progs;
if( !isset($progs) )
{
$progs = new progs();
}
return $progs;
}
A more Drupal way:
progs.module
/**
* Implements hook_init().
*/
function progs_init(){
global $progs;
// Consider using drupal_static to cache this
if( !isset($progs) )
{
module_load_include('inc', 'progs', 'progs');
$progs = new Progs();
}
}
progs.inc (convention is to use .inc)
class Progs
{
//a bunch of properties
function __construct()
{
// load partial includes and objects
$this->load_partial_inclues();
//retrieve all programs that are open
$this->open_programs = Program::getAllOpenPrograms();
}
function load_partial_inclues()
{
//includes
module_load_include('php', 'progs', 'core/objects/program');
}
}
I've created a plugins system, and I've created everything in that system except, how can I inclusion plugins files to execute it.
I'm tried to create a method, Which is doing include plugins files to execute it.
-- Firstly -- :
The method that get all plugins files, and that begin with index word which indicates the main file of plugin (i.g. index-pluginName.php), and add the path and file name to an array.
public function getPluginFiles($plugin_folder) {
$dir = opendir($plugin_folder);
while ($files = readdir($dir)) {
if ($files == '.' || $files == '..')
continue;
if (is_dir($plugin_folder.'/'.$files))
$this->getPluginFiles($plugin_folder.'/'.$files);
if (preg_match('/^[index]+/i', $files)) {
$this->plugins_path[$plugin_folder.'/'.$files] = $files;
}
}
closedir($dir);
}
-- secondly -- :
The method that include all the main file of plugins to execute, and this method get the path and name of plugin file from the array that created earlier .
public function includePlugFiles() {
$this->getPluginFiles($this->plugin_folder);
foreach ($this->plugins_path as $dir=>$file) {
include_once (dirname($dir)."/".$file);
}
}
Also see an example of code that exists in plugin file:
function test() {
echo " This is first plugin <br/>";
}
$plugin->addHook('top', test); // parameters(top=position, test=callback)
Now, when I create an instance of the object to be this form .
$plugin = new plugin;
$plugin->includePlugFiles();
But after all this, shows error message
Fatal error: Call to a member function addHook() on a non-object in .... projects\plugins\index-test.php on line 7
This is the code of line 7:
$plugin->addHook('top', test); // parameters(top=position, test=callback)
I know the problem occur because, the object will not be created.
and the problem is can't create the object in every main plugin file.
It's probably not the cleanest solution, but instead of trying to reference the $plugin symbol (which is outside the scope of the plugin file), you could also do this:
$this->addHook('top', test);
Alternatively, you could explicitly create the reference inside the includePlugFiles() method:
public function includePlugFiles()
{
$plugin = $this;
$this->getPluginFiles($this->plugin_folder);
foreach ($this->plugins_path as $dir=>$file) {
include_once (dirname($dir)."/".$file);
}
}
I have build a CMS using Zend Framework (1.11). In the application I have two modules, one called 'cms' which contains all the CMS logic and an other 'web' which enables a user to build their own website around the CMS. This involves adding controllers/views/models etc all in that module.
The application allows you to serve multiple instances of the app with their own themes. These instances are identified by the hostname. During preDispatch(), a database lookup is done on the hostname. Based on the database field 'theme' the app then loads the required css files and calls Zend_Layout::setLayout() to change the layout file for that specific instance.
I want to extend this functionality to also allow the user to run different web modules based on the 'theme' db field. However, this is where I am stuck. As it is now, the web module serves the content for all the instances of the application.
I need the application to switch to a different web module based on the 'theme' database value (so indirectly the hostname). Any ideas?
Well, in my opinion,
You should write a front controller plugin for the web module, and do it so, that when you need another plugin, you can do so easily.
The front controller plugin should look something like this:
class My_Controller_Plugin_Web extends My_Controller_Plugin_Abstract implements My_Controller_Plugin_Interface
{
public function init()
{
// If user is not logged in - send him to login page
if(!Zend_Auth::getInstance()->hasIdentity()) {
// Do something
return false;
} else {
// You then take the domain name
$domainName = $this->_request->getParam( 'domainName', null );
// Retrieve the module name from the database
$moduleName = Module_fetcher::getModuleName( $domainName );
// And set the module name of the request
$this->_request->setModuleName( $moduleName );
if(!$this->_request->isDispatched()) {
// Good place to alter the route of the request further
// the way you want, if you want
$this->_request->setControllerName( $someController );
$this->_request->setActionName( $someAction );
$this->setLayout( $someLayout );
}
}
}
/**
* Set up layout
*/
public function setLayout( $layout )
{
$layout = Zend_Layout::getMvcInstance();
$layout->setLayout( $layout );
}
}
And the My_Controller_Plugin_Abstract, which is not an actual abstract and which your controller plugin extends,looks like this:
class My_Controller_Plugin_Abstract
{
protected $_request;
public function __construct($request)
{
$this->setRequest($request);
$this->init();
}
private function setRequest($request)
{
$this->_request = $request;
}
}
And in the end the front controller plugin itself, which decides which one of the specific front controller plugins you should execute.
require_once 'Zend/Controller/Plugin/Abstract.php';
require_once 'Zend/Locale.php';
require_once 'Zend/Translate.php';
class My_Controller_Plugin extends Zend_Controller_Plugin_Abstract
{
/**
* before dispatch set up module/controller/action
*
* #param Zend_Controller_Request_Abstract $request
*/
public function routeShutdown(Zend_Controller_Request_Abstract $request)
{
// Make sure you get something
if(is_null($this->_request->getModuleName())) $this->_request->setModuleName('web');
// You should use zend - to camelCase convertor when doing things like this
$zendFilter = new Zend_Filter_Word_SeparatorToCamelCase('-');
$pluginClass = 'My_Controller_Plugin_'
. $zendFilter->filter($this->_request->getModuleName());
// Check if it exists
if(!class_exists($pluginClass)) {
throw new Exception('The front controller plugin class "'
. $pluginClass. ' does not exist');
}
Check if it is written correctly
if(!in_array('My_Controller_Plugin_Interface', class_implements($pluginClass))) {
throw new Exception('The front controller plugin class "'
. $pluginClass.'" must implement My_Controller_Plugin_Interface');
}
// If all is well instantiate it , and you will call the construct of the
// quasi - abstract , which will then call the init method of your
// My_Plugin_Controller_Web :)
$specificController = new $pluginClass($this->_request);
}
}
If you have never done this, now is the time. :)
Also, to register your front controller plugin with the application, you should edit the frontController entry in your app configuration. I will give you a json example, i'm sure you can translate it to ini / xml / yaml if you need...
"frontController": {
"moduleDirectory": "APPLICATION_PATH/modules",
"defaultModule": "web",
"modules[]": "",
"layout": "layout",
"layoutPath": "APPLICATION_PATH/layouts/scripts",
// This is the part where you register it!
"plugins": {
"plugin": "My_Controller_Plugin"
}
This might be a tad confusing, feel free to ask for a more detailed explanation if you need it.
I am writing an CSV/Excel-->MySQL import manager for an MVC application (Kohana/PHP).
I have a controller named "ImportManager" which has an action named "index" (default) which displays in a grid all the valid .csv and .xls files that are in a specific directory and ready for import. The user can then choose the files he wants to import.
However, since .csv files import into one database table and .xls files import into multiple database tables, I needed to handle this abstraction. Hence I created a helper class called SmartImportFile to which I send each file be it .csv or .xls and then I get then ask this "smart" object to add the worksheets from that file (be they one or many) to my collection. Here is my action method in PHP code:
public function action_index()
{
$view = new View('backend/application/importmanager');
$smart_worksheets = array();
$raw_files = glob('/data/import/*.*');
if (count($raw_files) > 0)
{
foreach ($raw_files as $raw_file)
{
$smart_import_file = new Backend_Application_Smartimportfile($raw_file);
$smart_worksheets = $smart_import_file->add_smart_worksheets_to($smart_worksheets);
}
}
$view->set('smart_worksheets', $smart_worksheets);
$this->request->response = $view;
}
The SmartImportFile class looks like this:
class Backend_Application_Smartimportfile
{
protected $file_name;
protected $file_extension;
protected $file_size;
protected $when_file_copied;
protected $file_name_without_extension;
protected $path_info;
protected $current_smart_worksheet = array();
protected $smart_worksheets = array();
public function __construct($file_name)
{
$this->file_name = $file_name;
$this->file_name_without_extension = current(explode('.', basename($this->file_name)));
$this->path_info = pathinfo($this->file_name);
$this->when_file_copied = date('Y-m-d H:i:s', filectime($this->file_name));
$this->file_extension = strtolower($this->path_info['extension']);
$this->file_extension = strtolower(pathinfo($this->file_name, PATHINFO_EXTENSION));
if(in_array($this->file_extension, array('csv','xls','xlsx')))
{
$this->current_smart_worksheet = array();
$this->process_file();
}
}
private function process_file()
{
$this->file_size = filesize($this->file_name);
if(in_array($this->file_extension, array('xls','xlsx')))
{
if($this->file_size < 4000000)
{
$this->process_all_worksheets_of_excel_file();
}
}
else if($this->file_extension == 'csv')
{
$this->process_csv_file();
}
}
private function process_all_worksheets_of_excel_file()
{
$worksheet_names = Import_Driver_Excel::get_worksheet_names_as_array($this->file_name);
if (count($worksheet_names) > 0)
{
foreach ($worksheet_names as $worksheet_name)
{
$this->current_smart_worksheet['name'] = basename($this->file_name).' ('.$worksheet_name.')';
$this->current_smart_worksheet['kind'] = strtoupper($this->file_extension);
$this->current_smart_worksheet['file_size'] = $this->file_size;
$this->current_smart_worksheet['when_file_copied'] = $this->when_file_copied;
$this->current_smart_worksheet['table_name'] = $this->file_name_without_extension.'__'.$worksheet_name;
$this->assign_database_table_fields();
$this->smart_worksheets[] = $this->current_smart_worksheet;
}
}
}
private function process_csv_file()
{
$this->current_smart_worksheet['name'] = basename($this->file_name);
$this->current_smart_worksheet['kind'] = strtoupper($this->file_extension);
$this->current_smart_worksheet['file_size'] = $this->file_size;
$this->current_smart_worksheet['when_file_copied'] = $this->when_file_copied;
$this->current_smart_worksheet['table_name'] = $this->file_name_without_extension;
$this->assign_database_table_fields();
$this->smart_worksheets[] = $this->current_smart_worksheet;
}
private function assign_database_table_fields()
{
$db = Database::instance('import_excel');
$sql = "SHOW TABLE STATUS WHERE name = '".$this->current_smart_worksheet['table_name']."'";
$result = $db->query(Database::SELECT, $sql, FALSE)->as_array();
if(count($result))
{
$when_table_created = $result[0]['Create_time'];
$when_file_copied_as_date = strtotime($this->when_file_copied);
$when_table_created_as_date = strtotime($when_table_created);
if($when_file_copied_as_date > $when_table_created_as_date)
{
$this->current_smart_worksheet['status'] = 'backend.application.import.status.needtoreimport';
}
else
{
$this->current_smart_worksheet['status'] = 'backend.application.import.status.isuptodate';
}
$this->current_smart_worksheet['when_table_created'] = $when_table_created;
}
else
{
$this->current_smart_worksheet['when_table_created'] = 'backend.application.import.status.tabledoesnotexist';
$this->current_smart_worksheet['status'] = 'backend.application.import.status.needtoimport';
}
}
public function add_smart_worksheets_to(Array $smart_worksheets = array())
{
return array_merge($smart_worksheets, $this->get_smart_worksheets());
}
public function get_smart_worksheets()
{
if ( ! is_array($this->smart_worksheets))
{
return array();
}
return $this->smart_worksheets;
}
}
In a code review I was told that it might be better not to have a helper class like this but to keep the bulk of the code in the controller action method itself. The argumentation was that you should be able to look at the code in a controller action and see what it does instead of having it call external helper classes outside of itself. I disagree. My argumentation is:
you should create a helper class anytime it makes code clearer, as in this case, it abstracts away the fact that some files have one worksheet or many worksheets in them, and allows for easy future extension, if, say, we want to also import from sqlite files or even directories with files in them, this class abstraction would be able to handle this nicely.
moving the bulk of the code from this helper class back into the controller would force me to create internal variables in the controller which make sense for this particular action, but may or may not make sense to other action methods within the controller.
if I were programming this in C# I would make this helper class a nested class which would literally be an internal data structure that is inside of and only available to the controller class, but since PHP does not allow nested classes, I need to call a class "outside" the controller to help manage this abstraction in a way that makes the code clear and readable
Based on your experience of programming in the MVC pattern, should the above helper class be refactored back into the controller or not?
There are two approaches to controllers: make it thin or thick. When I started my adventure with MVC I made a mistake of creating thick controllers - now I prefer make it as thin as possible. Your solution is good in my opinion.
Here is how I would redesigned your code even further:
class Backend_Application_SmartImport {
public function __construct( $raw_files ) {
}
public function process() {
foreach ($raw_files as $raw_file) {
// (...)
$oSmartImportFileInstance = $this->getSmartImportFileInstance( $smart_import_file_extension );
}
}
protected function getSmartImportFileInstance( $smart_import_file_extension ) {
switch ( $smart_import_file_extension ) {
case 'xml':
return new Backend_Application_SmartImportFileXml();
// (...)
}
}
}
abstract class Backend_Application_SmartImportFile {
// common methods for importing from xml or cvs
abstract function process();
}
class Backend_Application_SmartImportFileCVS extends Backend_Application_SmartImportFile {
// methods specified for cvs importing
}
class Backend_Application_SmartImportFileXls extends Backend_Application_SmartImportFile {
// methods specified for xls importing
}
The idea is to have two classes responsible for processing xml and cvs inheriting from a base class. The main class uses a special method to detect how the data should be processed (Strategy Pattern). The controller just passed a list of files to the instance of Backend_Application_SmartImport class and passes result of process method to the view.
The advantage of my solution is that code is more decoupled and you can easily and in a clean way add new types of processing like xml, pdf, etc.
I agree with you Edward.
Your ImportController does what a Controller is meant to do. It generates the list of files for the user to view and act on, it then passes that list to the View for it to display. I am presuming that you have a process action or similar which is handles the request when a user has selected a file, this file is then passed on to the Helper in question.
The Helper is a perfect example of abstraction and entirely justified in its usage and existence. It is not coupled with the Controller in anyway and doesn't need to be. The Helper could be easily used in other scenarios where the Controller is not present, for example a CRON task, a public API which users can call programmatically without your ImportController.
Your right on the ball with this one. Stick it to 'em!