Upgrade to Pro — share decks privately, control downloads, hide ads and more …

So, you wanna build an add-on...

So, you wanna build an add-on...

An introduction to building an ExpressionEngine Add-on.

A347386377e120e3230403d2558c1a0d?s=128

Lodewijk Schutte

October 17, 2012
Tweet

Transcript

  1. So, you wanna build an add-on… By Lodewijk Schutte ~

    Low, October 2012
  2. Hi! I’m Low!

  3. Developing Add-ons

  4. The Brief

  5. “Facebook likes, but without Facebook.”

  6. None
  7. 1. Analyze the Brief 2. Create Scaffold 3. Fill in

    the Gaps 4. Optimize & Simplify 5. Document it The Plan
  8. - Define features 1. Analyze the Brief

  9. A member can like and unlike an entry » many-to-many

    member-entry relationship: DB table » Add/remove records to/from this table » A form per entry Show number of likes for each entry » Get like-count for each entry Show liked entries for a logged in member » Show channel entries based on likes by member Show entries with most likes » Show channel entries sorted by number of likes
  10. Overview of recently liked entries » List of entry +

    member + date » DB table needs a timestamp Overview of members that liked an entry » For a single entry, show members that liked it Overview of entries liked by a member » For a single member, show entries that he/she liked Keep like-counts accurate » When an entry is deleted, remove its likes » When a member is deleted, remove his/her likes
  11. - Define features - Think of a name 1. Analyze

    the Brief
  12. Low Likes

  13. - Define features - Think of a name - Sketch

    out database tables, if needed 1. Analyze the Brief
  14. exp_low_likes » entry_id int(10) » member_id int(10) » like_date int(11)

  15. - Define features - Think of a name - Sketch

    out database tables, if needed - Create dummy template code 1. Analyze the Brief
  16. {exp:channel:entries/channel="products"} / <h2>{title}</h2> / {exp:low_likes:show/entry_id="{entry_id}"} / / <p> / /

    / {total_likes}/Like{if/total_likes/!=/1}s{/if} / / </p> / {/exp:low_likes:show} {/exp:channel:entries}
  17. {exp:channel:entries/channel="products"} / <h2>{title}</h2> / {exp:low_likes:show/entry_id="{entry_id}"/form="yes"} / / <p> / /

    / {total_likes}/Like{if/total_likes/!=/1}s{/if} / / / {if/has_form} / / / / <button/type="submit"> / / / / / {if/is_liked}Unlike{if:else}Like{/if} / / / / </button> / / / {/if} / / </p> / {/exp:low_likes:show} {/exp:channel:entries}
  18. {!EE/List/of/entries/liked/by/member/EE} {exp:low_likes:entries/[all$channel:entries$params]} / [all$channel:entries$vars] {/exp:low_likes:entries}

  19. {!EE/List/of/mostEliked/entries/EE} {exp:low_likes:popular/[all$channel:entries$params]} / [all$channel:entries$vars] {/exp:low_likes:popular}

  20. 2. Create Scaffold - Initiate package

  21. low_likes » Module upd.low_likes.php mod.low_likes.php mcp.low_likes.php language/english/low_likes_lang.php css/low_likes.css views/mcp_index.php views/mcp_entries.php

    views/mcp_members.php » Extension ext.low_likes.php » Package constants config.php
  22. None
  23. 2. Create Scaffold - Initiate package - Add placeholder methods

  24. Classes & Methods » low_likes_upd install() uninstall() update() » low_likes

    show() entries() popular() toggle_like() » low_likes_mcp index() entries() members() » low_likes_ext member_delete() delete_entries_loop() channel_entries_query_result()
  25. - Keep the EE & CI docs ready 3. Fill

    in the Gaps
  26. $thisE>EE/=&/get_instance();

  27. Development Style Guide http://bit.ly/EE-dev-guide

  28. About ACTions http://bit.ly/EE-ACTions

  29. Available Extension Hooks http://bit.ly/EE-hooks

  30. More docs... php.net mysql.com regular-expressions.info the ExpressionEngine files themselves

  31. - Keep the EE & CI docs ready - Outline

    methods with comments 3. Fill in the Gaps
  32. /** /*/This/method/takes/care/of/barring/the/foo /*/ public/function/foobar() { ///////Check/prerequisites ///////Prep/something/for/later/use ///////Query/database / ///////Do/some/calculations/based/on/query/results

    / ///////Finalize/output / ///////Return/output }
  33. - Keep the EE & CI docs ready - Outline

    methods with comments - Create and test install/uninstall routine 3. Fill in the Gaps
  34. ext.low_likes.php » Add property to move (un)installation to module public/$required_by/=/array('module');

    upd.low_likes.php » Install method Create/new/table Add/row/to/exp_modules/table Add/row/to/exp_actions/table Add/rows/to/exp_extensions/table » Uninstall method Remove/rows/from/exp_modules,/exp_actions,/ exp_extensions/and/exp_module_member_groups/tables Drop/table » Update method Nothing/yet...
  35. - Keep the EE & CI docs ready - Outline

    methods with comments - Create and test install/uninstall routine - Create the rest 3. Fill in the Gaps
  36. How to draw an owl Step 1: draw some circles

  37. How to draw an owl Step 1: draw some circles

    Step 2: draw the rest of the owl
  38. {exp:channel:entries/channel="products"} / <h2>{title}</h2> / {exp:low_likes:show/entry_id="{entry_id}"/form="yes"} / / <p> / /

    / {total_likes}/Like{if/total_likes/!=/1}s{/if} / / / {if/has_form} / / / / <button/type="submit"> / / / / / {if/is_liked}Unlike{if:else}Like{/if} / / / / </button> / / / {/if} / / </p> / {/exp:low_likes:show} {/exp:channel:entries}
  39. /** /*/Show/Likes/for/entry,/possibly/wrapped/in/a/form/tag /*/ public/function/show() { ///////Get/the/tagdata ///////Check/for/entry/ID/parameter,/bail/out/if/not/present ///////Query/database/for/this/entry's/likes ///////Create/variables/array /

    ///////Parse/the/vars/in/the/tagdata / ///////Wrap/tagdata/in/a/form/if/needed ///////Return/final/output }
  40. /** /*/Show/Likes/for/entry,/possibly/wrapped/in/a/form/tag /*/ public/function/show() { ///////Get/the/tagdata ////$tagdata/=/$thisE>EEE>TMPLE>tagdata; ///////Check/for/entry/ID/parameter,/bail/out/if/not/present ////if/(/!/($entry_id/=/$thisE>EEE>TMPLE> ////////////fetch_param('entry_id')))

    ////{ ///////////Add/message/to/template/log ////////$thisE>EEE>TMPLE>log_item('Low/Likes:/no/entry_id'); ////////return/$tagdata; ////} ///////Query/database/for/this/entry's/likes ///////Create/variables/array /
  41. ////} ///////Query/database/for/this/entry's/likes ///////Create/variables/array / ///////Parse/the/vars/in/the/tagdata / ///////Wrap/tagdata/in/a/form/if/needed ///////Return/final/output }

  42. ///////Query/database/for/this/entry's/likes ////$likes/=/array(); ////$query/=/$thisE>EEE>dbE>select('member_id') ///////////E>from('low_likes') ///////////E>where('entry_id',/$entry_id) ///////////E>get(); ////foreach/($queryE>result()/AS/$row) ////{ ////////$likes[]/=/$rowE>member_id; ////}/

    ///////Create/variables/array / ///////Parse/the/vars/in/the/tagdata / ///////Wrap/tagdata/in/a/form/if/needed ///////Return/final/output }
  43. ///////Query/database/for/this/entry's/likes ////$likes/=/array(); ////$query/=/$thisE>EEE>dbE>select('member_id') ///////////E>from('low_likes') ///////////E>where('entry_id',/$entry_id) ///////////E>get(); ////foreach/($queryE>result()/AS/$row) ////{ ////////$likes[]/=/$rowE>member_id; ////}/

    ///////Create/variables/array ////$member_id/=/$thisE>EEE>sessionE>userdata('member_id'); ////$form/=/(($thisE>EEE>TMPLE>fetch_param('form')/==/'yes') //////////////&&/$member_id); ////$vars/=/array( ////////'total_likes'/=>/count($likes), ////////'is_liked'////=>/in_array($member_id,/$likes),
  44. ////}/ ///////Create/variables/array ////$member_id/=/$thisE>EEE>sessionE>userdata('member_id'); ////$form/=/(($thisE>EEE>TMPLE>fetch_param('form')/==/'yes') //////////////&&/$member_id); ////$vars/=/array( ////////'total_likes'/=>/count($likes), ////////'is_liked'////=>/in_array($member_id,/$likes), ////////'has_form'////=>/$form ////);

    / ///////Parse/the/vars/in/the/tagdata ////$tagdata/=/$thisE>EEE>TMPLE>parse_variables_row($tagdata, ///////////////////$vars); / ///////Wrap/tagdata/in/a/form/if/needed ///////Return/final/output }
  45. ///////////////////$vars); / ///////Wrap/tagdata/in/a/form/if/needed ///////Return/final/output }

  46. ///////Wrap/tagdata/in/a/form/if/needed ////if/($form) ////{ ///////////Initiate/data/array/for/form/creation ////////$fd/=/array( ////////////'id'////=>/$thisE>EEE>TMPLE>fetch_param('form_id'), ////////////'class'/=>/$thisE>EEE>TMPLE>fetch_param('form_class') ////////); ///////////Define/default/hidden/fields ////////$fd['hidden_fields']/=/array(

    ////////////'ACT'/=>/$thisE>EEE>functionsE>fetch_action_id( /////////////////////'Low_likes',/'toggle_like'), ////////////'EID'/=>/$entry_id ////////); ///////////Wrap/form/around/tagdata ////////$tagdata/=/$thisE>EEE>functionsE>form_declaration($fd) /////////////////./$tagdata /////////////////./'</form>'; ////}
  47. ///Wrap/tagdata/in/a/form/if/needed if/($form) { ///////Initiate/data/array/for/form/creation ////$fd/=/array( ////////'id'////=>/$thisE>EEE>TMPLE>fetch_param('form_id'), ////////'class'/=>/$thisE>EEE>TMPLE>fetch_param('form_class') ////); ///////Define/default/hidden/fields ////$fd['hidden_fields']/=/array(

    ////////'ACT'/=>/$thisE>EEE>functionsE>fetch_action_id( /////////////////'Low_likes',/'toggle_like'), ////////'EID'/=>/$entry_id ////); ///////Wrap/form/around/tagdata ////$tagdata/=/$thisE>EEE>functionsE>form_declaration($fd) /////////////./$tagdata /////////////./'</form>'; }
  48. ///////Wrap/tagdata/in/a/form/if/needed ////if/($form) ////{ ///////////Initiate/data/array/for/form/creation ////////$fd/=/array( ////////////'id'////=>/$thisE>EEE>TMPLE>fetch_param('form_id'), ////////////'class'/=>/$thisE>EEE>TMPLE>fetch_param('form_class') ////////); ///////////Define/default/hidden/fields ////////$fd['hidden_fields']/=/array(

    ////////////'ACT'/=>/$thisE>EEE>functionsE>fetch_action_id( /////////////////////'Low_likes',/'toggle_like'), ////////////'EID'/=>/$entry_id ////////); ///////////Wrap/form/around/tagdata ////////$tagdata/=/$thisE>EEE>functionsE>form_declaration($fd) /////////////////./$tagdata /////////////////./'</form>'; ////}
  49. ///////////Initiate/data/array/for/form/creation ////////$fd/=/array( ////////////'id'////=>/$thisE>EEE>TMPLE>fetch_param('form_id'), ////////////'class'/=>/$thisE>EEE>TMPLE>fetch_param('form_class') ////////); ///////////Define/default/hidden/fields ////////$fd['hidden_fields']/=/array( ////////////'ACT'/=>/$thisE>EEE>functionsE>fetch_action_id( /////////////////////'Low_likes',/'toggle_like'), ////////////'EID'/=>/$entry_id

    ////////); ///////////Wrap/form/around/tagdata ////////$tagdata/=/$thisE>EEE>functionsE>form_declaration($fd) /////////////////./$tagdata /////////////////./'</form>'; ////} ///////Return/final/output ////return/$tagdata; }
  50. <form/method="post"/action="http://ee250.dev/"> ////<div/class='hiddenFields'> ////////<input/type="hidden"/name="XID" /////////value="333115662decf4c778adb673a0e9f9535f02134a"//> ////////<input/type="hidden"/name="ACT"/value="25"//> ////////<input/type="hidden"/name="EID"/value="29570"//> ////////<input/type="hidden"/name="site_id"/value="1"//> ////</div> ////<p>0/Likes/<button/type="submit">Like</button></p> </form>

  51. /** /*/ACT:/(un)like/posted/entry /*/ public/function/toggle_like() { ///////Get/entry_id/from/POST,/member_id/from/session ////$entry_id/=/$thisE>EEE>inputE>post('EID'); ////$member_id/=/$thisE>EEE>sessionE>userdata('member_id'); ///////Only/continue/if/both/are/valid ////if/($entry_id/&&/$member_id)

    ////{ ///////////Create/data/array/for/DB/queries ////////$data/=/array( ////////////'entry_id'//=>/$entry_id, ////////////'member_id'/=>/$member_id ////////); ///////////Is/entry/liked/by/member? ////////$thisE>EEE>dbE>where($data); ////////$liked/=/$thisE>EEE>dbE>count_all_results('low_likes');
  52. ///////////If/liked,/delete/entry;/else,/insert/entry ////////if/($liked) ////////{ ////////////$thisE>EEE>dbE>delete('low_likes',/$data); ////////} ////////else ////////{ ////////////$data['like_date']/=/$thisE>EEE>localizeE>now; ////////////$thisE>EEE>dbE>insert('low_likes',/$data); ////////}

    ///////////Cater/for/Ajax/requests ////////if/(AJAX_REQUEST) ////////{ ////////////die($liked/?/'E1'/:/'1'); ////////} ////} //////// ///////Return/to/previous/page ////$thisE>EEE>functionsE>redirect( ////////$thisE>EEE>sessionE>tracker[1] ////);
  53. {exp:channel:entries/channel="products"/limit="25"} / <h2>{title}</h2> / {exp:low_likes:show/entry_id="{entry_id}"/form="yes"} / / <p> / /

    / {total_likes}/Like{if/total_likes/!=/1}s{/if} / / / {if/has_form} / / / / <button/type="submit"> / / / / / {if/is_liked}Unlike{if:else}Like{/if} / / / / </button> / / / {/if} / / </p> / {/exp:low_likes:show} {/exp:channel:entries}
  54. 0.0005 SELECT `member_id` FROM (`exp_low_likes`) WHERE `entry_id` = '29570' 0.0004

    SELECT `member_id` FROM (`exp_low_likes`) WHERE `entry_id` = '29533' 0.0003 SELECT `member_id` FROM (`exp_low_likes`) WHERE `entry_id` = '29532' 0.0003 SELECT `member_id` FROM (`exp_low_likes`) WHERE `entry_id` = '29530' 0.0003 SELECT `member_id` FROM (`exp_low_likes`) WHERE `entry_id` = '29531' 0.0004 SELECT `member_id` FROM (`exp_low_likes`) WHERE `entry_id` = '29529' 0.0004 SELECT `member_id`
  55. 4. Optimize & Simplify - Apply caching

  56. /** /*/Add/likeEvars/to/channel:entries/data /*/ public/function/channel_entries_query_result($obj,/$entries) { ///////Get/the/latest/version/of/$entries ///////Is/there/a/low_likes/tag/here? ///////Initiate/likes/for/all/given/entry_ids ///////Query/DB/for/these/entry_ids ///////Set/cache

    ///////Return/entries }
  57. /** /*/Add/likeEvars/to/channel:entries/data /*/ public/function/channel_entries_query_result($obj,/$entries) { ///////Get/the/latest/version/of/$entries ////if/($thisE>EEE>extensionsE>last_call/!==/FALSE) ////{ ////////$entries/=/$thisE>EEE>extensionsE>last_call; ////}

    ///////Is/there/a/low_likes/tag/here? ////if/(/!/strpos($thisE>EEE>TMPLE>tagdata,/'exp:low_likes:')) ////{ ////////return/$entries; ////} ///////Initiate/likes/for/all/given/entry_ids ////$likes/=/array(); ////foreach/($entries/AS/$row) ////{
  58. ////foreach/($entries/AS/$row) ////{ ////////$likes[$row['entry_id']]/=/array(); ////} ///////Query/DB/for/these/entry_ids ////$query/=/$thisE>EEE>dbE>select('entry_id,/member_id') ///////////E>from('low_likes') ///////////E>where_in('entry_id',/array_keys($likes)) ///////////E>get(); ////foreach/($queryE>result()/AS/$row)

    ////{ ////////$likes[$rowE>entry_id][]/=/$rowE>member_id; ////} ///////Set/cache ////$thisE>EEE>sessionE>set_cache('Low',/'likes',/$likes); ///////Return/query ////return/$entries; }
  59. ///////Query/database/for/this/entry's/likes ////$likes/=/array(); ////$query/=/$thisE>EEE>dbE>select('member_id') ///////////E>from('low_likes') ///////////E>where('entry_id',/$entry_id) ///////////E>get(); ////foreach/($queryE>result()/AS/$row) ////{ ////////$likes[]/=/$rowE>member_id; ////}/

  60. ///////Query/database/for/this/entry's/likes ///////$likes/=/array(); ///////$query/=/$thisE>EEE>dbE>select('member_id') //////////////E>from('low_likes') //////////////E>where('entry_id',/$entry_id) //////////////E>get(); ///////foreach/($queryE>result()/AS/$row) ///////{ ////////$likes[]/=/$rowE>member_id; ///////}

    ///////Get/likeEdata/from/cache ////$likes/=/$thisE>EEE>sessionE>cache('Low',/'likes'); ///////Gets/for/this/entry/only ////$likes/=/(array)/@$likes[$entry_id];
  61. 0.0003 SELECT `entry_id`, `member_id` FROM (`exp_low_likes`) WHERE `entry_id` IN (29570,

    29533, 29532, 29530, 29531, 29529, 29528, 29527, 29526, 29525, 29524, 29523, 29522, 29521, 29520, 29519, 29518, 29517, 29516, 29515, 29514, 29513, 29512, 29511, 29510)
  62. 4. Optimize & Simplify - Apply caching - Pat it

    DRY
  63. /** /*/Show/entries/liked/by/logged/in/member /*/ public/function/entries() { ///////Get/entry/ids/for/logged/in/member/from/DB ///////Set/fixed_order/parameter/based/on/ids ///////Load/&/call/the/Channel/module } /**

    /*/Show/entries/with/most/likes /*/ public/function/popular() { ///////Get/entry/ids/and/member/count/from/DB ///////Set/fixed_order/parameter/based/on/ids ///////Load/&/call/the/Channel/module }
  64. /** /*/Show/entries/liked/by/logged/in/member /*/ public/function/entries() { ///////Get/entry/ids/for/logged/in/member/from/DB ///////Set/fixed_order/parameter/based/on/ids ///////Load/&/call/the/Channel/module } /**

    /*/Show/entries/with/most/likes /*/ public/function/popular() { ///////Get/entry/ids/and/member/count/from/DB ///////Set/fixed_order/parameter/based/on/ids ///////Load/&/call/the/Channel/module }
  65. /** /*/Show/entries/liked/by/logged/in/member /*/ public/function/entries() { ///////Get/entry/ids/for/logged/in/member/from/DB } /** /*/Show/entries/with/most/likes /*/

    public/function/popular() { ///////Get/entry/ids/and/member/count/from/DB } /** /*/Load/and/call/Channel::entries() /*/ private/function/_get_entries($entry_ids) { ///////Set/fixed_order/parameter/based/on/ids ///////Load/&/call/the/Channel/module
  66. /*/ public/function/entries() { ///////Get/entry/ids/for/logged/in/member/from/DB } /** /*/Show/entries/with/most/likes /*/ public/function/popular() {

    ///////Get/entry/ids/and/member/count/from/DB } /** /*/Load/and/call/Channel::entries() /*/ private/function/_get_entries($entry_ids) { ///////Set/fixed_order/parameter/based/on/ids ///////Load/&/call/the/Channel/module }
  67. Other ways to not repeat yourself » Use a “base”

    class and inheritance » Create your own libraries, models, and helpers » Use “shortcuts” for frequently requested config items Simplify, but also keep it simple » Choose readability over elegance » Separate add-on data from the core
  68. 5. Document it - If you haven’t already

  69. Wanna learn more? Add-on development screencasts coming soon to mijingo.com

  70. Thank You! gotolow.com @low