HackerOne users: Testing against this community violates our program's Terms of Service and will result in your bounty being denied.
How do I write a Plugin with a Module?
This discussion was created from comments split from: Template Errors.
Check out the Documentation! We are always looking for new content and pull requests.
Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.
3
Comments
Oh boy, I wish I could write my own Vanilla plugins! I wanted to learn how to do it for quite some time now, but never got around it.
I had to implement a countdown button in the side panel for our forum just recently, which was when I discovered that I can't just insert PHP code into the default master template file.
My search for more info brought me to this discussion, and further on how to implement a custom Smarty hook. So I wrote a
function.countdown.php
and put it into thelibrary/vendors/SmartyPlugins
folder. That solution is working just fine right now on my production site, but of course I know it's just not the proper way to do. If I could convert it to a plugin, I would be able to manage the countdown from the dashboard instead of having to enter the data directly as parameters in the Smarty hook in the template file. Hopefully at one point I find the time to play around with that idea...Just start, it's easy! And if you have written the logic and encapsulated it in smarty, you can transform it easily into a module. Look at that tutorial: http://vanillawiki.homebrewforums.net/index.php/How_to_Build_a_Simple_Module_and_Integrate_it_as_a_Plugin
Come back at any time and ask if you get stuck
@R_J, cool, a tutorial is exactly what I was looking for next. Will give it a read in my spare time, thanks.
It's so easy, let's do it here together
And if it is ready, we could ask some mod to split the discussion and move it to the tutorials.
I'd split it up into 3 steps which are also the files that you need:
1. Create the plugin:
/plugins/Countdown/class.countdown.plugin.php
2. You want to create a module, so let's create the file:
/plugins/Countdown/class.countdownmodule.php
3. You want it to be configurable, so let's add a settings screen:
/plugins/Countdown/views/settings.php
Look at the link I've posted: step 1 is super easy! A plugin must have an information block and a class that has to be named YourPluginNamePlugin. So first lets take a look at the information block:
Pretty self explanatory.
A description array could (and should) contain more information, but it doesn't need to have more lines. So let's keep it simple and start with that. We will expand it after everything is up and running.
Then, there is the plugin. Our plugin doesn't do anything fancy. It just loads the module, which is where the program logic is. Here is all that is needed:
That's all we have to do for step 1! Great, isn't it? Here's a short explanation of each line:
class CountdownPlugin extends Gdn_Plugin {
We create our own class which must be named like our file and our PluginInfo array. Filename should be all lowercase but beware that the class name and the PluginInfo must be exactly the same.
public function Base_Render_Before($Sender) {
You can take control of what is happening during script execution at many times: whenever an "event" is "fired". Other software calls such events "hooks" but the principle is the same. If you are interested in finding out which events there are, I'd strongly recommend the plugin Eventi. It's a great tool!
Our function expects a parameter called $Sender. That was the hard part for me to understand. If you look into the source code of Vanilla for
FireEvent
you'll find the places where we have the possibility to hook into script execution. Sometimes it is inside of a view and we could add some characters to the screen or change what should be displayed, sometimes it is inside of a model and we can alter the query that will be used to get some data, or it is at any other place. $Sender will always be the class instance (is that the correct wording?) of the class where the event has been fired.if (GetValue('Panel',$Sender->Assets) && $Sender->MasterView != 'admin') {
Let's start with what an "Asset" is: if you look at a default.master.tpl, you'll normally find two assets: content and panel.
I think it's quite clear what they are and it is even more clear that if we want to create a module, we only want to continue in code execution if there is a "Panel" asset in the class we are looking at.
If the "MasterView" is "admin", we are looking at the dashboard. I'm not sure if we need this part, because I think there is no Panel in the dashboard. You could experiment with that - go and play!
$CountdownModule = new CountdownModule($Sender);
"CountdownModule" will be the name of a class we create in step 2. All we are doing here is creating an instance.
$Sender->AddModule($CountdownModule);
In one of the previous steps we made sure that the class we have hooked has an asset called panel. We wanted to insert our own module into the panel and that's the Vanilla function to do so.
For completeness, Here's the complete code for class.countdown.plugin.php (which could also be named as "default.php" but I prefer the more descriptive name)
That has been step 1. Quite easy, isn't it? I guess step 2 will not follow today, except you are doing it alone. Here are some guys that can guide you if you take the challenge
Well, I'm in teaching mode, so here is step2: class.countdownmodule.php
A module should do/show something and you have already written some code. Mind sharing it with us? We could implement it in the skeleton I'll construct in this post.
Here we go. As I've said above, we need to create our own class and there is a naming convention we should take into account. Garden, the framework behind Vanilla, has some "base" classes that we normally extend when we are writing a plugin (no one has to start at zero, here). In step 1 we have used Gdn_Plugin and now that we are writing our module, we do this:
class CountdownModule extends Gdn_Module {
I guess there's nothing left to say...
A module should have 2 functions "AssetTarget" that will hold the information where we want our module to show up and "ToString" which handles the output of the module.
That was all we have to do to tell Vanilla that we want our module to show up in the panel. Not much functionality, not much code. I like that.
So what have we done here? We've created the function ToString that I've mentioned above and let it echo some html. Beware! You do not have to return the string, you have to echo it! But maybe it works with a return also - it's up to you to do some testing again...
Modules headings are h4 and they have the div.Box container so you should simply copy the styling of the standard modules in order to have a nice looking forum.
Well, that's it. Not very spectacular, so let me simply repeat what I've said before:
1. extend class Gdn_Module
2. create a function "AssetTarget" that defines where this module should be shown
3. create function "ToString" that echos out the html you want to include in your panel
Not much more to say. It's really that simple. Here's the code for class.countdownmodule.php
You can now try to implement your markup inside of the ToString function already
By the way: modules are very flexible and there are lot of ways to achieve the same. You don't really need a dedicated AssetTarget or ToString function, but it is far more easier to teach it that way.
Step 3 will be the config screen, but I'll wait until you tell us what should be in there.
I think we'll do a step 4 which will include localization.
Edit: sorry, markup is fucked up - just search for "<_" and replace by "<"
Wow, you are amazing, @R_J, this will be really a great help not only for me. Right now I really can't start playing around with this (have to work), but in the meantime I can let you know how exactly I have implemented that countdown functionality with my "hacky" style.
library/vendors/SmartyPlugins/function.countdown.php
The folder
themes/name_of_theme/design
contains 31 PNGs named from00_days.png
to31_days.png
and the following addition in custom.css:Then in
themes/name_of_theme/views/default.master.tpl
it was just placing the new Smarty hook at the right place:Here you can also see the parameters I'd like to have configurable in the dashboard.
Since you do not know how to use the configuration by now, use something like that in the ToString function:
We'll take care for those vars in the next step.
Very well, @R_J, the two files are in place now, I'm fine with
$CountdownTarget
, so let's stick with that one.If possible, can you please also show how to set permissions for accessing and configuring that plugin? I'd like users of a certain role to be able to set and start the countdown.
We'll do that later on. Let me show you what is "normally" done and then we can tune one or two aspects to your needs.
For example I wouldn't go with 31 pictures but either only one picture using css sprites (http://csssprites.com/) or using a nicely styled standard font.
By the way: I will not write any "countdown" code - that's completely up to you. I show you how you could make your code into a module. It will be more satisfying seeing your own code running in your forum ;-)
Moreover, I see one thing in your code that needs you to understand what happened in step 1&2 in order to implement in a clean way. So doing that by yourself will surely help you getting comfortable with coding for Vanilla.
I start writing step3 down, but weather is great and I think I will be out most of the day...
Step 3 will be the hardest and longest part. Not because using the configuration is difficult, but because using forms include more steps by itself:
1. design a form
2. insert configuration values when the form is loaded
3. save form values to configuration when a button is hit
First let me begin with some general comments about configuration. You can store individual settings anywhere but there is a convention for plugin related settings. They should be saved inside
conf/config.php
. If you look at that file, you may seem some other plugins configurations. When we are finished, you will find your $CountdownTitle like that inside the config:$Configuration['Plugins']['Countdown']['Title'] = 'Something NEW is coming!';
Afterwards you'll be able to do something like this
echo C('Plugins.Countdown.Title', 'Optional default title if this setting is not yet made in the config');
. That will either echo the config setting or - if that could not be found - the default we've specified.If you want to initially set a config value or change what is set in there, you can do it like that
SaveToConfig('Plugins.Countdown.Title', 'Shiny new title');
Knowing that, you can already make your plugin configurable! I've told you to set the $CountdownSomething variables in the ToString function. Instead of assigning string values to them, you can get the values from the config:
$CountdownSomething = C('Plugins.Countdown.Something');
. But you still have to change the config.php manually and we wanted to have a dedicated settings screen.Before we start with the form itself, let's take a look at the plugin. We have to add only one single line to the PluginInfo in order to get a (not-yet-functional) settings button to the plugin list where we can see our new plugin.
Only the last line is new. Check the plugin list to see the new settings button. The url of this button is
/dashboard/plugin/countdown
and that could be read as "dashboard" = current application, "plugin" = controller and "countdown" = function in the plugin controller. Yes, we point our settings screen to a function inside of Garden controller. This might sound strange, but we have a way to define/create custom functions "inside" of controllers so that they will be treated as if they belong to them. Let's do exactly that:public function PluginController_Countdown_Create($Sender) {
We Create the function Countdown to be a part of the PluginController with that line. If you just put in the line
echo 'Overwhelming!'; die;
inside of this function and press the settings button in the plugins overview, you'll see how easy it is to create a setting screen! But that's not how we will continue...$Sender->Permission('Garden.Settings.Manage');
By just inserting that line, the current users permissions are checked and if he hasn't the requested right, he'll see an error screen. You've asked for custom permissions and so we will come back later to this line. The requested permission here is a general permission.
$Sender->AddSideMenu('dashboard/settings/plugins');
That line will highlight the "Plugins" entry in the side menu, but you do not need to do it.
$Sender->Form = new Gdn_Form();
Garden has a lot of helpers that you can use when dealing with forms. A setting screen is a form and that's why we attach a form to our controller.
$Validation = new Gdn_Validation();
Validation is one other helper that we will use. You can do a lot with that, but we just need it for the ConfigurationModel...
$ConfigurationModel = new Gdn_ConfigurationModel($Validation);
There's magic in there. The "Model" in MVC is the part that stores and retrieves data. ConfigurationModel does exactly that and it's "backend" is the config file. We plan on getting information from the config and write to it and so having a model for that makes accessing the config quite easy. We have to give a special rule set to the ConfigurationModel and that rule set is defined in the Validation. That sounds quite complicated and I have to admit that I can not write such code down by heart. I always have to look at other plugins for code like that. But you do not have to understand each single step in order to start writing your first plugin.
As I've said, the ConfigurationModel gives us access to the config.php but we certainly do not want to handle each value in there, but only the fields of your plugin. So we specify what those are.
$Sender->Form->SetModel($ConfigurationModel);
We can link a form and a model! Think about it: if there is a connection between a data source and input fields, it is obvious that we will not have much work to do. Displaying the stored values can be done by creating html elements with the correct ids (more on "correct" later) and the values of a form could be easily saved back. That's exactly what we've needed.
if ($Sender->Form->AuthenticatedPostBack() === FALSE) {
Here we check if the form already contains data that has been sent back to us or if we see the form for the first time.
$Sender->Form->SetData($ConfigurationModel->Data);
If the above check result was that we see the form for the first time, we want to see the current values of our defined fields in the form. I guess we can call that magic again: $ConfigurationModel->Data gets us an array of the desired config values. Form->SetData sets each form element that has an id which is included in the result array to the value of that array element! So that single line will prepopulate our form fields with the config values, great isn't it?
If we do not see form for the first time, then the form has been submitted back to us and we now have to save it. We've linked the ConfigurationModel to the Form and so if we call the forms save function, the values will also be written to the config. A one-liner again! But we also check that the return process hasn't failed. If there was no error, we give a status message back to the user so that he knows that everything worked as expected.
$Sender->Render($this->GetView('settings.php'));
We've talked a lot about the form, defined the functionality behind it and now it is time to load and display it. The GetView function looks at several places for the specified file name. One of those places is the views subdirectory of the current files directory.
That has been a lot of stuff although it has been not much more than a dozen lines of code. The form itself will be a piece of cake now that we have taken that step!
Here comes the form (
/plugins/Countdown/views/settings.php
):(and I had to replace "<" by "<_" again)
I will not explain every line since there's a lot of repetition and some trivial html in there. If you've read my previous post thoroughly, you'll be able to understand every single line immediately.
{$this->Form->Open()}
This will write an opening html form tag with the correct action (the current forms url) and method ("post") for us.
{$this->Form->Errors()}
I've spoken about "validation" before and if some of the input fields would be sent with content that doesn't adhere to the validation rules, the error message will be written back here with this line.
{$this->Form->Label('Countdown Title', 'Plugins.Countdown.Title')}
and{$this->Form->TextBox('Plugins.Countdown.Title')}
There are some predefined form elements that we are using here. The Form->Label creates a label html element with the text "Countdown Title" and a for reference to "Plugins.Countdown.Title".
The above Form->TextBox creates an input type text with the name "Plugins.Countdown.Title". Naming the input field like that is a requisite! Only if the html input elements have the same names as the config settings, the ConfigModel and the Form can work hand in hand.
(The name "Plugins.Countdown.Title" gets converted to "Plugins-dot-Countdown-dot-Title" for the html element, by the way, but you don't have to care for that)
We enclose our form elements in an ordered list. Why? Simply because the wise creators of Vanilla have decided to use it like that and so we have a predefined style for that. For this example I started by enclosing the form elements in paragraphs, but ended up looking in official plugins what markup has been used there. Copy whenever you can copy
{$this->Form->Label('Countdown Days', 'Plugins.Countdown.Days')}
{$this->Form->Date('Plugins.Countdown.Days')}
If you look close you'll see that we are using a form element called "Date" that you do not know from html. Correct. Get an inspiration of what form elements are accessible by looking at
/library/core/class.form.php
. There is some fine tuning possible for the Date "element" that we'll look at later on. What is important for you is to know that it is saved as "YYYY-MM-DD" inside of the config. In your example you've given an exact time. Think if this is really necessary.There's a lot more that you can know about each single step, but I think I've told you everything you have to knw to create a plugin that shows a module and has a configuration screen. I'll go through the code and give additional information to a few points. If something is not working as expected, give a short feedback and we'll get it up and running!
We've started with PluginInfo and I want to elaborate just a little bit on it again. There are quite a lot of options that you can specify here and we started with barebone, but there are some more that I would recommend to you:
'MobileFriendly' => TRUE
That decides if your plugin will be loaded for the mobile theme also. Default seems to be FALSE
`'License' => 'Whatever your preferred license is'
That will make it easier for other, honest authors to work with your code
'AuthorEmail' => '...',
'AuthorUrl' => '...',
Just in case someone still has a question
'HasLocale' => TRUE
Make your plugin translatable! And perhaps include a translation file. We will do this for your plugin, too. But to be honest: that setting has no meaning at all. It is just a convention and not requested at any time ;-)
'RequiredApplications' => array('Vanilla' => '2.1'),
'RequiredTheme' => FALSE,
'RequiredPlugins' => FALSE,
Those are some other PluginInfo fields that might be of interest at some time. I would include the RequiredApplications in order to stress that you haven't tested you plugin with older versions.
OK, I have put it all in place. Some things I'd like to comment on:
I have implemented it with single pictures because it was the easiest that I can do on a short notice. Have a look at my test setup >>> here <<< to see my hacky-style implemention at work. That's exactly how I wanted it to look like, and I could not figure out a way to place the text in that button-link by just using the empty button as background and put the text in with some formating magic.
Not sure about the advantage of converting it to a single CSS sprites picture.
I think you meant the
Date
element the other way around:Unfortunately it is necessary to have the time provided for the countdown calculation, because the day-switch has to happen at a certain daytime in a different time zone.
Maybe the time should be put in its own variable and input field?
Looks like this will be a little tricky, because in my desired end result the countdown is shown at the top of the side panel column below the site logo when in full mode, but put in the footer in line with the site logo when in mobile mode, because the mobile template has no side panel column (you can see the desired result in my above mentioned test setup).
I really appreciate your outstanding help with this matter, it would have definitely taken a lot of time to figure this all out by myself if at all.
CSS sprites only make sense for small graphics, so they wouldn't be the right solution in your case. Look at that short CSS hack: http://codepen.io/anon/pen/wEyLq
Colors and size are not what you need, but I think it is a start to use pure CSS for what you are doing right now with a picture.
Yes, I've mixed up Date and Day. If you need the time, you have a lot of options:
1. use a normal text input and either trust the input or do some validations
2. use the Date element and add another element: maybe a select field so that you can choose the time with a dropdown?
3. search for a javascript date time picker like that: http://www.ama3.com/anytime/ It transforms a normal TextBox so that you can use all the advantages of Vanilla.
I assume there is no problem with the mobile version. Try to set MobileFriendly to FALSE. It should be possible to include the module manually in your mobile themes default.master.tpl. Look at that code snippet: http://vanillaforums.org/discussion/comment/206387/#Comment_206387
Just add the panel to your mobile theme...
❌ ✊ ♥. ¸. ••. ¸♥¸. ••. ¸♥ ✊ ❌
@R_J, a plethora of new information I have to process first, really cool stuff! Pure CSS really could be it, but is the shown CSS hack cross-browser compatible? With CSS I'm always afraid having to make specific adaptations for various browsers to have it look the same whichever browser the visitor has in use.
@vrijvlinder, interesting idea, though the solution as it is implemented now and seen in the test installation mentioned above (without panel and logo links / countdown button at the bottom) pretty much already suits our needs. I guess you mean a side panel in a mobile theme that is hidden to have the full screen estate for the discussions and only gets visible on demand? I think that will confuse most users, because they are more used getting a menu when something opens, and not a whole panel. Is there any ready to test mobile theme which has it implemented that way?
No, I mean that if your mobile theme does not have the panel, simply add it to the tpl. then anything that is on the panel will also work for the mobile.
Mobile themes by default don't contain the panel. I added it to my themes because I wanted to have the panel and it's contents also available for mobile.
Adding the panel to the mobile will not make the page wider. It will simply appear above or bellow the content depending on where you add it in the master. Then add css for it to make it width 100% that will force it to the size of the mobile window.
All my mobile themes have the panel . Just pick one and see how it's put together.
Yellow-Mobile and Mobile are tpl based themes
the others are php based mobile themes in case you want to compare those .
❌ ✊ ♥. ¸. ••. ¸♥¸. ••. ¸♥ ✊ ❌
I wanted to go through the code and make some annotations.
I've told you that your module needs two functions, but gave you a hint that they could be replaced. While I would always stick to the ToString function, I guess it would not be bad to replace AssetTarget.
AssetTarget determines where your module should be rendered. Gave you following code:
A function that returns only one value is no great function at all and so I think it is wise to think of other options to provide this information. Do you remember how we have added our module to the controller in the plugin code?
$Sender->AddModule($CountdownModule);
. So let's take a closer look at that "function AddModule". It is always helpful to have the source code at hand and do a search. We will find the following line public function AddModule($Module, $AssetTarget = '') {">public function AddModule($Module, $AssetTarget = '') {
Obviously we could specify the AssetTarget already when we add our module to the controller! That's why you could replace
$Sender->AddModule($CountdownModule);
with$Sender->AddModule($CountdownModule, 'Panel');
and don't need that small function AssetTarget inside the module any more!Maybe something general again. I've said several times in this posting, that we added the module to the controller while you may be convinced that we've added it to the output. Yes, you are certainly right. But in order to get a full understanding of Vanilla, you need to have at least a few insights of what a MVC framework is. In fact it is no true MVC framework but it is usually called that way. Since it is not really MVC, it should be better called CMV: the browser passes the request to the Controller, the controller gets data from the Model and then gets the appropriate View. Our function AddModule is a function of the controller object that tells the controller how (where) to render our module. The controller is the traffic policeman that tells which code has to go where...
vrijvlinder already told you that you can add the Panel asset to your mobile theme and that's a good solution, but it might lead to more modules inside of your mobile theme than you wanted. If that is the case, create a new asset in your mobile theme. Something like
{asset name="MobileFooter"}
.Then you can do the following in your module or in the plugin (although I've told you to get rid of the function AssetTarget, I'll use it again for this example because it is a standard function and in this case there will be logic inside of it):
edit: using a non standard asset name will make your plugin unusable for others!
Don't know much about CSS. Check cross compatibility here: http://caniuse.com/#cats=CSS
Speaking of CSS: when you want to style your module, you should create a css file and save it in the subfolder "design" of your plugin. Call it "countdown.css" or "custom.css". Now we want to add that css just like we have added our module with $Sender->AddModule, right? Here is how it is done: put that code below the $Sender->AddModule line
$Sender->AddCssFile('countdown.css', 'plugins/Countdown');
Just like we have added the module to the controller, we can add also our css file to it! We pass the name of the css file and the path to our plugin to it and the function will automatically guess that we have saved that file in our "design" folder.
That's comfortable, isn't it?
One possible solution for your timestamp problem was the usage of some Javascript - guess what our next step is
We have seen that there is a convenient function for adding a css file, so wouldn't it be nice if there is something similar for javascript? At first we need the file somewhere. Create the subfolder "js" in your plugins directory. And then, just from the code above, what would you guess is the right function and syntax?
$Sender->AddJsFile('yourfile.js', 'plugins/Countdown');
is what you have guessed and you are right!But to put that line in the function where we attach the module to the controller doesn't make sense. We need the js file when our settings screen is loaded and that happened in our
public function PluginController_Countdown_Create($Sender)
. So put the line inside of that function and when you take a look at the html source of your settings screen, you will see our file has been loaded.