HackerOne users: Testing against this community violates our program's Terms of Service and will result in your bounty being denied.

Some tips I learned during plugin development

lifeisfoolifeisfoo Zombie plugins finder ✭✭✭

Paths

In a recent discussion I outlined that (sorry for the autoreference):

(plugins) use hardcoded directory name inside .php files

I say that since many plugins that I've seen use this approach:

include_once(PATH_PLUGINS.DS . 'MyPluginDirectoryName' . DS . 'class.myclass.php');

Not so clean, since you can use (beware of yourself):

include_once(dirname(FILE) . DS . 'class.myclass.php');

Views paths

When you need to set a view, you can use a similar approch:

$Sender->View = dirname(FILE) . DS . 'views' . DS . 'myview.php';

>

$Sender->Render();

or a (better? I found it just now) method form the class.plugin.php

$Sender->Render($this->GetView('myview.php'));

Note: Please contribute below with your own tips or with your critique.

Thank you to @hgtonight for this discussion.

There was an error rendering this rich post.

Comments

  • KasperKasper Scholar of the Bits Copenhagen Vanilla Staff
    edited May 2013

    Great post - glad you're learning new stuff about plugin development! If I may, I have a few things to add:

    Paths

    First off, relying on __FILE__ or any derivatives of $_SERVER variables is a bad habit. I learned this the hard way when I started working for Vanilla - things can go awry when moving code with $_SERVER variables from one environment to another.

    The first, and probably foremost, reason is that not all web servers provide you with $_SERVER variables and some web servers may only provide you with some and omit others. The second reason is that $_SERVER variables are pretty oblivious to symlinks: These are automatically resolved, thus in most cases resulting in unreachable files.

    Lastly, I personally "never" (exceptions occur) use any include or require statements in my code. I rely solely on autoloading and would usually write my own spl_autoload_register functions before discovering how to include stuff directly in the Garden autoloader - http://vanillaforums.org/discussion/23402/how-to-register-files-and-folders-in-the-garden-autoloader-2-1

    View paths

    Again, the stuff I wrote about $_SERVER vars also applies here. I'm not sure if the following is the case in 2.0, but this is how I include views in my 2.1 plugins:

    $this->FetchView('fooview', 'barfolder', 'plugins/FooBar');
    

    This would fetch the following view (plugins/FooBar and barfolder can be omitted in many cases):
    plugins/FooBar/views/barfolder/fooview.ext where ext can be either tpl or php.

    I think that was my 50 cents!

    Kasper Kronborg Isager (kasperisager) | Freelance Developer @Vanilla | Hit me up: Google Mail or Vanilla Mail | Find me on GitHub

  • hgtonighthgtonight ∞ · New Moderator

    My key take away from this is that you don't need to include module files IF you place it in the modules folder and call it class._custom_module.php.

    Search first

    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.

  • businessdadbusinessdad Stealth contributor MVP

    @hgtonight said:
    My key take away from this is that you don't need to include module files IF you place it in the modules folder and call it class._custom_module.php.

    In my experience, you can place the files wherever you like, as long as you call the file class.yourclassname.php. This applies to Modules, Models and almost any class name (e.g. class.thisisafancyclass.php would be automatically loaded too).

    Controllers are handled slightly differently, as it seems that the Autoloader looks in specific folders for them. I seem to remember that @kasperisager posted a way to register your own controllers, or something like that. When I developed my Bagdes plugin, I took a shortcut, and avoided using the word "Controller" in my classes. A bit dirty, but it works. :)

  • lifeisfoolifeisfoo Zombie plugins finder ✭✭✭
    edited May 2013

    Remember functions.general.php

    Sometimes I need to get some specific information or to transform data...and I must write many lines of code. But many useful functions to do that are already in functions.general.php...

    A comprehensive list from 2.1

       function AbsoluteSource($SrcPath, $Url) {
       function AddActivity($ActivityUserID, $ActivityType, $Story = '', $RegardingUserID = '', $Route = '', $SendEmail = '') {
       function ArrayCombine($Array1, $Array2) {
       function array_fill_keys($Keys, $Val) {
       function ArrayHasValue($Array, $Value) {
       function ArrayKeyExistsI($Key, $Search) {
       function ArrayInArray($Needle, $Haystack, $FullMatch = TRUE) {
       function ArraySearchI($Value, $Search) {
       function ArrayTranslate($Array, $Mappings, $AddRemaining = FALSE) {
       function ArrayValue($Needle, $Haystack, $Default = FALSE) {
       function ArrayValueI($Needle, $Haystack, $Default = FALSE) {
       function ArrayValuesToKeys($Array) {
       function Asset($Destination = '', $WithDomain = FALSE, $AddVersion = FALSE) {
       function Attribute($Name, $ValueOrExclude = '') {
       function C($Name = FALSE, $Default = FALSE) {
       function CTo(&$Data, $Name, $Value) {
       function CalculateNumberOfPages($ItemCount, $ItemsPerPage) {
       function ChangeBasename($Path, $NewBasename) {
       function CheckPermission($PermissionName, $Type = '') {
       function CheckRestriction($PermissionName) {
       function MultiCheckPermission($PermissionName) {
       function CheckRequirements($ItemName, $RequiredItems, $EnabledItems, $RequiredItemTypeCode) {
       function check_utf8($str) {
       function CombinePaths($Paths, $Delimiter = DS) {
       function CompareHashDigest($Digest1, $Digest2) {
       function ConcatSep($Sep, $Str1, $Str2) {
       function ConsolidateArrayValuesByKey($Array, $Key, $ValueKey = '', $DefaultValue = NULL) {
       function decho($Mixed, $Prefix = 'DEBUG', $Permission = FALSE) {
       function filter_input($InputType, $FieldName, $Filter = '', $Options = '') {
       function DateCompare($Date1, $Date2) {
       function Debug($Value = NULL) {
       function DebugMethod($MethodName, $MethodArgs = array()) {
       function Deprecated($Name, $NewName = FALSE) {
       function ExternalUrl($Path) {
       function FetchPageInfo($Url, $Timeout = 3) {
       function DomGetContent($Dom, $Selector, $Default = '') {
       function DomGetImages($Dom, $Url, $MaxImages = 4) {
       function fnmatch($pattern, $string, $flags = 0) { 
       function pcre_fnmatch($pattern, $string, $flags = 0) { 
       function ForceIPv4($IP) {
       function ForeignIDHash($ForeignID) {
       function FormatString($String, $Args = array()) {
       function _FormatStringCallback($Match, $SetArgs = FALSE) {
       function ForceBool($Value, $DefaultValue = FALSE, $True = TRUE, $False = FALSE) {
       function ForceSSL() {
       function ForceNoSSL() {
       function FormatArrayAssignment(&$Array, $Prefix, $Value) {
       function FormatDottedAssignment(&$Array, $Prefix, $Value) {
       function getallheaders() {
       function GetAppCookie($Name, $Default = NULL) {
       function GetConnectionString($DatabaseName, $HostName = 'localhost', $ServerType = 'mysql') {
       function GetIncomingValue($FieldName, $Default = FALSE) {
       function GetMentions($String) {
       function GetObject($Property, $Object, $Default) {
       function GetPostValue($FieldName, $Default = FALSE) {
       function GetRecord($RecordType, $ID) {
       function GetValue($Key, &$Collection, $Default = FALSE, $Remove = FALSE) {
       function GetValueR($Key, $Collection, $Default = FALSE) {
       function HtmlEntityDecode($string, $quote_style = ENT_QUOTES, $charset = "utf-8") {
       function chr_utf8_callback($matches) { 
       function chr_utf8($num) {
       function ImplodeAssoc($KeyGlue, $ElementGlue, $Array) {
       function InArrayI($Needle, $Haystack) {
       function InSubArray($Needle, $Haystack) {
       function IsMobile($Value = NULL) {
       function IsSearchEngine() {
       function IsTimestamp($Stamp) {
       function IsUrl($Str) {
       function IsWritable($Path) {
       function MarkString($Needle, $Haystack) {
       function MergeArrays(&$Dominant, $Subservient) {
       function Now() {
       function OffsetLimit($OffsetOrPage = '', $LimitOrPageSize = '') {
       function PageNumber($Offset, $Limit, $UrlParam = FALSE, $First = TRUE) {
       function parse_ini_string ($Ini) {
       function RecordType($Row) {
       function write_ini_string($Data) {
       function write_ini_file($File, $Data) {
       function SignInPopup() {
       function ParseUrl($Url, $Component = -1) {
       function BuildUrl($Parts) {
       function PrefixString($Prefix, $String) {
       function PrepareArray($Key, &$Array, $PrepareType = 'array') {
       function ProxyHead($Url, $Headers=NULL, $Timeout = FALSE, $FollowRedirects = FALSE) {
       function ProxyRequest($Url, $Timeout = FALSE, $FollowRedirects = FALSE) {
       function RandomString($Length, $Characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') {
       function BetterRandomString($Length, $CharacterOptions = 'A0') {
       function Redirect($Destination = FALSE, $StatusCode = NULL) {
       function ReflectArgs($Callback, $Args1, $Args2 = NULL) {
       function RemoteIP() {
       function RemoveFromConfig($Name, $Options = array()) {
       function RemoveKeyFromArray($Array, $Key) {
       function RemoveKeysFromNestedArray($Array, $Matches) {
       function RemoveQuoteSlashes($String) {
       function RemoveValueFromArray(&$Array, $Value) {
       function SafeGlob($Pattern, $Extensions = array()) {
       function SafeImage($ImageUrl, $MinHeight = 0, $MinWidth = 0) {
       function SafeParseStr($Str, &$Output, $Original = NULL) {
       function SaveToConfig($Name, $Value = '', $Options = array()) {
       function SetAppCookie($Name, $Value, $Expire = 0, $Force = FALSE) {
       function SliceParagraph($String, $MaxLength = 500, $Suffix = '…') {
       function SliceString($String, $Length, $Suffix = '…') {
       function SmartAsset($Destination = '', $WithDomain = FALSE, $AddVersion = FALSE) {
       function StringBeginsWith($Haystack, $Needle, $CaseInsensitive = FALSE, $Trim = FALSE) {
       function StringEndsWith($Haystack, $Needle, $CaseInsensitive = FALSE, $Trim = FALSE) {
       function StringIsNullOrEmpty($String) {
       function SetValue($Key, &$Collection, $Value) {
       function T($Code, $Default = FALSE) {
       function Theme() {
       function TouchValue($Key, &$Collection, $Default) {
       function TouchFolder($Path, $Perms = 0777) {
       function Trace($Value = NULL, $Type = TRACE_INFO) {
       function TrueStripSlashes($String) {
       function TrueStripSlashes($String) {
       function Url($Path = '', $WithDomain = FALSE, $RemoveSyndication = FALSE) {
       function ViewLocation($View, $Controller, $Folder) {
       function PasswordStrength($Password, $Username) {
    

    Wait, not only you can use them

    But you can override them in /conf/bootstrap.before.php, changing the way vanilla works (since these functions are used in the entire codebase). They are loaded conditionally from functions.general.php and if you re-define (e.g.) function T($Code, $Default = FALSE) in your bootstrap file, your version will be used.

    There was an error rendering this rich post.

  • lifeisfoolifeisfoo Zombie plugins finder ✭✭✭

    Adding controller actions from a plugin

    It's an easy task, just add a function to the plugin class:

    public function ProfileController_MyAction_Create($Sender){
        //do something
      }
    

    But, wait...

    We need to take care of other plugins hooks. Because an existing plugin that have a ProfileController_BeforeRenderAsset_Handler can rely, for example, on the $Sender->User field (I ALWAYS use Gdn::Session()->User), since it is present in the ProfileController. But when the hook is called for the new action, the field isn't set, and this can generate many errors in other plugins.

    So, what to do?

    Just analyze controller's fields and take care to provide all public fields. In this case (ProfileController):

    public function ProfileController_MyAction_Create($Sender){
        $Sender->User = Gdn::UserModel()->GetID(Gdn::Session()->UserID);
        //do something
      }
    

    How can I encounter this error?

    Developing a plugin for a client (will be released soon) that also uses @peregrine PeregrineBadges :-). See ProfileController_BeforeRenderAsset_Handler function in the plugin.

    There was an error rendering this rich post.

  • peregrineperegrine MVP
    edited June 2013

    @lifeisfoo said:

    Adding controller actions from a plugin

    It's an easy task, just add a function to the plugin class:

    public function ProfileController_MyAction_Create($Sender){
        //do something
      }
    

    But, wait...

    We need to take care of other plugins hooks. Because an existing plugin that have a ProfileController_BeforeRenderAsset_Handler can rely, for example, on the $Sender->User field (I ALWAYS use Gdn::Session()->User), since it is present in the ProfileController. But when the hook is called for the new action, the field isn't set, and this can generate many errors in other plugins.

    So, what to do?

    Just analyze controller's fields and take care to provide all public fields. In this case (ProfileController):

    public function ProfileController_MyAction_Create($Sender){
        $Sender->User = Gdn::UserModel()->GetID(Gdn::Session()->UserID);
        //do something
      }
    

    How can I encounter this error?

    Developing a plugin for a client (will be released soon) that also uses peregrine PeregrineBadges :-). See ProfileController_BeforeRenderAsset_Handler function in the plugin.

    not sure what you mean, in this case sender-user was not the same as sesssion user, they were two different entities from what I recallm and two different userids. I didn't re-analyze it,

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • lifeisfoolifeisfoo Zombie plugins finder ✭✭✭

    Adding a js file from application's class.hooks.php

    Application folder: /applications/myapp
    Js file: /applications/myapp/js/my.js

    $Sender->AddJsFile('my.js', '/application/myapps'); //no
    $Sender->AddJsFile('my.js', 'application/myapps'); //no
    $Sender->AddJsFile('my.js'); //no
    $Sender->AddJsFile('my.js', 'myapps'); //YES
    

    Just read Controller::RenderMaster() source code ;)

    There was an error rendering this rich post.

Sign In or Register to comment.