Solve a complex Problem (rationally)
To solve complex problems, there is a kind of standardized/well known approach: Divide your complex problem into a set of smaller problems. Smaller problems are easier to solve. If you have solved each smaller problem you most often have solved your complex problem already.
Locate and Divide into Parts
So far for the theory. Let\'s check your needs. I line-up in my own words what you described above:
- Handle a set of options in multiple instances (slide-1 to slide-N).
- Store values in post meta values.
- A Metabox in multiple instances (slide-1 to slide-N).
- A Slideshow Editor (Form) within the Metabox (again multiple Instances)
- A Basic Form to edit the Data (e.g. using simple HTML Form Elements)
- The technical problem to solve, having multiple WYSIWYG editors.
- Your more complex Image Input.
So how to code that? I think the most important part is that before you start to actual coding, you make up your mind what you really want to achieve and how to divide the problem into the smaller parts. The list above might not be complete it\'s just what I could read from your question. And the format is quite unspecific. It\'s just more or less a repetition from what you wrote but a bit ordered into single points.
So check if those are your needs and extend that list to all you see fit.
When done, the next step is to look how those needed stuff could be expressed in simple words and as a list of features of your plugin
- A Plugin for this: SlideshowPlugin.
- A Slideshow that consists of Slides.
- An Editor to edit a Slideshow.
- A place to store and retrieve the Slideshow data from.
Looks quite simple now, right? I probably might have missed something here, so double check on your own before you continue. As written, before starting to code just make up your mind in simple wording and terms. Don\'t even think about which parts are problematic and which are not or how to code some detail like the naming of HTML Input elements. I know that\'s hard if you already tried for such a long time now to restart from scratch because there are many ideas in your mind\'s back that come up again.
Grab a pencil and some paper. This often helps to make up someone\'s mind.
As you can see I did not specify the need of a Metabox or a Custom Post Type here. It\'s too specific to learn about the parts of your problem first. Metabox or Custom Post Type is very concrete, possibly something how to code the plugin already. So I kept this out for the moment and tried to short but precisely describe the needs. The Metabox or similar is something which might play a role in the Design. Let\'s see.
Design your Plugin
After you know what you need/want to achieve, you can make up the mind about how to design the plugin. This could be done by drawing a little picture. Just Identify the parts from your Feature List and set them in relation to each other. As you can see, you actually do not need to do fine arts ;) :
Excuse my bad writing, I hope this can be read. In any case, you can create your own image, this is just an example. Designs can vary, so if you would not draw it the same way, that\'s just normal.
I like to do the beginning of the design step on paper because it helps to get a better view on the problem from above and it\'s much faster on paper then on the computer.
So now you could compare your list with your design and check if all features from the list are covered by the parts you have in the design. Looks good so far for my list and my image, so I continue, but don\'t skip this step. Otherwise you do not know if you have missed something. And as you start to code soon, it\'s much more hard to change something that\'s already coded than an image or a list.
Separation of Problems in the Design
Now this plugin becomes more concrete in ones mind. After some little design, it\'s probably the right time to start coding. As we have the list on top, I could go through and think about each point on it\'s own and cross-check with the Design so that I know how the parts are in relationship to each other.
Because if every single part is done, the plugin is ready without making up the mind on the whole thing at once, which was the original problem.
I do the coding now a bit in comment style and some sample code. It\'s a first implementation idea and the code is untested. It\'s just to get the hands dirty for me and for you to probably have an example of how - but not must - this can be written. I tend to be too specific sometimes already, so mind me. When I write code, I re-write it quite often while creating it, but I can not make this visible while creating the sample code. So bear this in mind. If you see something to be done simpler, choose your route. You need to change and extend your code later on, so this is really only some example code.
Plugin
Just a class that handles the basic operation like registering hooks and providing the Slideshow with it\'s Slides and the Metaboxes for the Editor. This is where everything starts. Plugins are started at a single point of code. I call that bootstrap:
<?php
/** Plugin Headers ... */
return SlideshowPlugin::bootstrap();
class SlideshowPlugin {
/** @var Slideshow */
private $slideshow;
/** @var SlideshowMetaboxes */
private $metaboxes;
/** @var SlideshowPlugin */
static $instance;
static public function bootstrap() {
$pluginNeeded = (is_admin() && /* more checks based your needs */ );
if (!$pluginNeeded)
return;
}
if (NULL === self::$instance) {
// only load the plugin code while it\'s really needed:
require_once(\'library/Slideshow.php\');
require_once(\'library/SlideshowSlide.php\');
require_once(\'library/Store.php\');
require_once(\'library/Metaboxes.php\');
require_once(\'library/Metabox.php\');
require_once(\'library/Form.php\');
// ...
self::$instance = new SlideshowPlugin();
}
return self::$instance;
}
private function addAction($action, $parameters = 0, $priority = 10) {
$callback = array($this, $action);
if (!is_callable($callback)) {
throw new InvalidArgumentExeception(sprintf(\'Plugin %s does not supports the %s action.\', __CLASS__, $action));
}
add_action($action, $callback, $parameters, $priority);
}
public function __construct() {
$this->addAction(\'admin_init\');
}
/**
* @return bool
*/
private function isEditorRequest() {
// return if or not the request is the editor page
}
/**
* @-wp-hook
*/
public function admin_init() {
// register anything based on custom post type and location in the admin.
// I don\'t care about the details with CPT right now.
// It\'s just that editorAction is called when we\'re on the slideshow
// editor page:
if ($this->isEditorRequest()) {
$this->editorAction();
}
}
private function getPostID() {
// ... code goes here to get post id for request
return $postID;
}
private function getSlideshow() {
is_null($this->slideshow)
&& ($postID = $this->getPostID())
&& $slideshow = new Slideshow($postID)
;
return $slideshow;
}
private function getMetaboxes() {
is_null($this->metaboxes)
&& ($slideshow = $this->getSlideshow())
&& $this->metaboxes = new SlideshowMetaboxes($slideshow)
;
return $this->metaboxes;
}
private function editorAction() {
$metaboxes = $this->getMetaboxes();
}
}
So this plugin class is already quite complete. I did not specify how to retrieve the postID but it\'s already encapsulated in a function on it\'s own. Next to that I did not code to check whether or not this is the right page to display the editor, but there\'s already some stub code for that.
In the end, the editorAction() is called if the request is on the actual custom post type editor page and in there, the Metaboxes are aquired. That\'s it. Plugin should be pretty complete now. It has the slideshow and takes care of the Metaboxes. Compared with the design, those are the parts which are linked with the plugin. Compare with the image:
- The decision whether or not the Editor is to be displayed.
- The connection between the plugin and the slideshow. The plugin has a slideshow already.
- The connection to the Metaboxes. The plugin has the Metaboxes already.
Looks complete. Job done on that part.
Slideshow and Slides
A Slideshow is 1:1 mapped to a post. So it needs to have the Post ID. The Slideshow can take care of holding the data, so it\'s basically a Datatype. It stores all the values you need there. It\'s a compound datatype in the sense that it consist have 0 to N slides. A Slide again is another Datatype that holds the information for each Slide.
The slide then is used by a metabox and probably a form.
I additionally choosed to implement the storage and retrieval of the slideshow data as well into those datatypes (the box labeled Store in the design). That\'s somehow dirty as it mixes datatypes and actual objects.
But as the Store is connected to Slideshow and Slides only in the Design I connected them with each other. Probably too close for the future, but for a first implementation of the design, I think it\'s a valid approach for the moment. As this is the first approach it won\'t take much time after it will get refactored anyway so even with some issues I\'m quite confident that the direction is right:
class SlideshowSlide {
private $slideshow;
private $index;
public $number, $hide, $type, $title, $image, $wysiwyg, $embed
public function __construct($slideshow, $index) {
$this->slideshow = $slideshow;
$this->index = $index;
}
public function getSlideshow() { return $this->slideshow; }
public function getIndex() { return $this->index; }
}
class Slideshow implements Countable, OuterIterator {
private $postID;
private $slides = array();
private function loadSlidesCount() {
$postID = $this->postID;
// implement the loading of the count of slides here
}
private function loadSlide($index) {
$postID = $this->postID;
// implement the loading of slide data here
$data = ... ;
$slide = new SlideshowSlide($this, $index);
$slide->setData($data); // however this is done.
return $slide;
}
private function loadSlides() {
$count = $this->loadSlidesCount();
$slides = array();
$index = 0;
while(($index < $count) && ($slide = $this->loadSlide($index++)))
FALSE === $slide || $slides[] = $slide
;
$this->slides = $slides;
}
public function __construct($postID) {
$this->postID = $postID;
$this->loadSlides();
}
public function count() {
return count($this->slides);
}
public function getInnerIterator() {
return new ArrayIterator($this->slides);
}
private function touchIndex($index) {
$index = (int) $index;
if ($index < 0 || $index >= count($this->slides) {
throw new InvalidArgumentExpression(sprintf(\'Invalid index %d.\', $index));
}
return $index;
}
public function getSlide($index) {
$index = $this->touchIndex($index);
return $this->slides[$index];
}
}
The Slideshow and Slide classes are also quite complete but lacks actual code as well. It\'s just to show my idea of having the properties / methods and some handling stuff as well on how the retrieval of data could be implemented.
Metabox
The Metabox needs to know which Slide it represents. So it needs to know the Slideshow and the concrete Slide. The Slideshow can be provided by the Plugin, the Slide could be represented by the index (e.g. 0 ... N where N is count of slides in the slideshow - 1).
class Metabox {
public function __construct(SlideshowSlide $slide) {
}
}
The Metabox class is actually extending the plugin class somehow. It does some of the work that could be done by the plugin class as well, but as I wanted to have it represent the slide in context of the plugin while being able to have multiple instances, I choosed this way.
The Metabox now needs to take care of the request logic: It represents one Metabox which is actually somehow output but it\'s also input as it needs to deal with form input.
The good thing is, it actually does not deal with the details because form output and input are done by the form objects.
So probably if I would have coded this class to an end, I would have removed it completely. I don\'t know right now. For the moment it represents the Metabox on the editor page for one specific slide.
Metaboxes
class Metaboxes
private $slideshow;
private $boxes;
public function __construct(Slideshow $slideshow) {
$this->slideshow = $slideshow;
$this->editorAction();
}
private function createMetaboxes() {
$slides = $this->slideshow;
$boxes = array();
foreach($slides as $slide) {
$boxes[] = new Metabox($slide);
}
$this->boxes = $boxes;
}
private function editorAction() {
$this->createMetaboxes();
}
I only wrote some little code here so far. The Metaboxes class acts as a manager for all metaboxes. The representative of the Slideshow as Metabox represents a slide.
That stub code does not much but instantiating one Metabox per Slide. It can and must do more in the end.
You probably might want to make use the of the Composite Pattern here, so to do an action on a Metaboxes object will do the same action on every Metabox it carries. Comparable with the instantiation, where it creates new Metaboxes. So you don\'t need to deal with individual Metaboxes later on, just with the Metaboxes object. Just an Idea.
Form
The Form is probably the most complex thing you have in terms of dealing with stuff and lines of code. It needs to abstract your data to be processed via the Browser. So it must be able to handle multiple instances. You can achieve this by prefixing form element names (as they need to be unique) with a genreal prefix (e.g. "slide"), then an identifier (the slide index, I name it index here as you want to be able to change the number e.g. to have a sort key) and then the actual value identifier (e.g. "number" or "hide").
Let\'s see: A form knows about it\'s prefix, the slide\'s number and all fields it contains. This pretty much maps to the Slideshow and Slide datatypes spoken about above. Some little stub-code:
/**
* SlideForm
*
* Draw a Form based on Slide Data and a Form definition. Process it\'s Input.
*/
class SlideForm {
/** @var Slide */
private $slide;
private $prefix = \'slide\';
public function __construct(Slide $slide) {
$this->slide = $slide;
}
private function inputNamePrefix() {
$index = $this->slide->getIndex();
$prefix = $this->prefix;
return sprintf(\'%s-%d-\', $prefix, $index);
}
private function inputName($name) {
return $this->inputNamePrefix().$name;
}
private function printInput(array $element) {
list($type, $parameters) = $element;
$function = \'printInput\'.$type;
$callback = array($this, $function)
call_user_func_array($callback, $parameters);
}
private function printInputText($value, $name, $label, $size = 4) {
$inputName = $this->inputName($name);
?>
<label for="<?php echo $inputName ; ?>">
<?php echo htmlspecialchars($label); ?>:
</label>
<input type="text"
name="<?php echo $inputName; ?>"
size="<?php echo $size; ?>"
value="<?php echo htmlspecialchars($value); ?>">
<?php
}
private function printInputCheckbox($value, $name, $label, ... ) { ... }
private function printInputRadio($value, $name, $label, ... ) { ... }
private function printInputImage($value, $name, $label, ... ) { ... }
private function printInputWysiwyg($value, $name, $label, ... ) { ... }
private function printInputTextarea($value, $name, $label, ... ) { ... }
// ...
private function mapSlideValueTo(array $element) {
$slide = $this->slide;
list($type, $parameters) = $element;
list($name) = $parameters;
$value = $slide->$name;
array_unshift($parameters, $value);
$element[1] = $parameters;
return $element;
}
/**
* @return array form definition
*/
private function getForm() {
// Form Definition
$form = array(
array(
\'Text\',
array(
\'number\', \'Number\'
),
array(
\'Checkbox\',
array(
\'hide\', \'Display\', \'Hide This Slide\'
),
),
array(
\'Radio\',
array(
\'type\', \'Type\', array(\'Image\', \'Video\')
),
),
array(
\'Text\',
array(
\'title\', \'Title\', 16
),
),
// ...
);
return $form;
}
public function printFormHtml() {
$form = $this->getForm();
foreach($form as $element) {
$element = $this->mapSlideValueTo($element);
$this->printInput($element);
}
}
public function processFormElement($element) {
list($type, $parameters) = $element;
list($name) = $parameters;
$inputName = $this->inputName($name);
$map = array(
\'Text\' => \'String\',
\'Checkbox\' => \'Checkbox\',
\'Radio\' => \'Radio\',
);
// I would need to continue to code there.
throw new Exception(\'Continue to code: Process Form Input based on Form Definition\');
}
public function processForm() {
$form = $this->getForm();
foreach($form as $element) {
$this->processFormElement($element); // <- this function needs to be coded
}
}
// ...
}
This class is quite large now because it takes care of three things at once:
- Form Definition
- Form Output Rendering
- Form Input Processing
It is wiser to split this up into the three parts it represents. I leave that up to you. It does already show how you can encapsulate the forms functionality into smaller tasks so it\'s easier to comply with your needs. E.g. in case the Image Input element form output needs tweaks, it can be easily extended / polished. Same for WYSIWYG. You can change the implementation later on as it won\'t interfere for you slideshow and slide datatypes much.
The principle behind this is also called Separation of Concerns, and that is just about how I started my answer: Divide the problem into smaller problems. Those separated problems are easier to solve.
I hope this helps for the moment. In the end I didn\'t come back to Custom Post Types even. I know they must go inside the plugin, but with a new design it\'s probably easy to find the place where to write the code to.
What\'s left?
- Split the code into multiple files. One class per file. You can merge them together later easily, but for development, keep things apart as you want to solve the smaller problems / parts on their own.
- Testing: You need to test the functionality of the parts on their own. Is the slide doing what it should do? Gladly you can make use of Unittests if you write classes, for PHP there is PHPUnit. This helps you to develop the code while knowing that it does exactly what it does. This helps to solve the simple problems in a unit of each own and you can more easily change your code later on as you can run tests all the time.
- Testing #2: Sure you need to test your plugin as a whole as well so you know that it is doing what you\'re looking for. Can you imagine how that can be done automatically so you can repeat the test with the same quality all the time?