Drupal CMS for marketing sites
Intro Sample sites: End to End flow Folder Structure Project setup Content Folder Data Store (Drupal CMS) Importing/Exporting Content Database Migrations Backend Config Unit Testing Routing Data binding Data validation Frontend Javascript Libraries Css Stylesheets Messages Common Smarty variables from controllers Variable references from Javascript Deployment What is included in the deployments Packaging with maven (if required) Creating a tag release and version increment Training resources Drupal: Smarty: PHP:
Intro The structure for the Drupal CMS marketing sites is a different setup to a typical drupal CMS build. The applications are designed to have a clear separation between frontend and cms. This design is so that we can strip out the Drupal CMS at any time with a different cms and not have to change the frontend of the websites. This allows a lot more freedom on the application builds and no strong dependencies on the CMS. Sample sites: Hahn Facebook app: http://www.hahn.com.au Direct desktop access: http://www.hahn.com.au/fbapptab?liked=lk_1234 Mobile version: http://www.hahn.com.au/mobileapp James Boag James Boag's Premium: http://jamesboagspremium.com.au James Boag's Draught: upcoming James Boag's Masterbrand: upcoming James Squire Main website: upcoming Corporate Lion corporate site: upcoming Lion career site: upcoming End to End flow The end to end flow of the website structure happens in the below diagram. The web request comes into the server, the htaccess routes through to a controller php file. A php service model is then retrieved from the facade, this model will talk to the json services from drupal cms. The data is then set into a model object with key value pairs, which is then rendered through a smarty template. The smarty template is then output as a string to the client as html.
Folder Structure The projects are maven projects so a specific folder structure must be adhered to. The diagram below has highlighted some key folders in the structure which should have specific files stored in.
Project setup Each project has it's own unqiue database name, user and password. This should be defined in the readme.md of the project root. When setting up your development machine make sure to match your user and password to that defined in the readme.md. This is because we have multiple developers working on the project at the same time and want everyone to have the same settings for when files are checked in and out of source control we do not override the settings. Content Folder Every project has it's own content folder which is stored outside of the drupal cms folder. This is so we can easily make deployments to the different servers without worrying about the content folder being overridden. To have this setup on your local dev machine first download a copy of the content folder from the staging or live environment.
Then when you create your virtual host in apache reference the folder by the environment variable WEBDEV_CONTENT. This can be seen in the virtual host configuration below. The content alias is pointing to the content folder and the WEBDEV_CONTENT variable is pointing to it's parent folder. This is because in drupal settings.php we are creating content reference with WEBDEV_CONTENT/{project name}. <VirtualHost *:80 > ServerName hahn.dev.local DocumentRoot "/home/aiden/development/gitrepos/lio0035_hahnpioneeringbeering/hahnpioneeringbeeringwebapp/src/main/webapp" DirectoryIndex index.php index.html index.htm <Directory "/home/aiden/development/gitrepos/lio0035_hahnpioneeringbeering/hahnpioneeringbeeringwebapp/src/main/webapp"> AllowOverride all Order allow,deny Allow from all </Directory> <Directory "C:/opt/temp/DevSitesContent/HahnPioneeringBeering"> AllowOverride all Order allow,deny Allow from all </Directory> Alias /content "C:/opt/temp/DevSitesContent/HahnPioneeringBeering" SetEnv WEBDEV_CONTENT "C:/opt/temp/DevSitesContent" </VirtualHost> Data Store (Drupal CMS) Drupal CMS is used as a data store only. There are no templates or frontend code coming from the CMS. This is a slightly different way of thinking to the ordinary CMS setup where everything comes from the CMS. The only information that should be stored in the CMS is the editable content (data only). For instance in http://www.hahn.com.au the editable content is the modules. The content editor wanted the ability to change the modules in the page with the image details, copy etc. This data is then retrieved from Drupal using the services module and outputting that information through json feeds. The setup of the data is quite simple where we have Node Content Types with properties of the data we want to store, Image Styles for the resizing of images for the frontend, Taxonomy for tagging content to use as filters. Views are then setup to output those Content Types and Taxonomy Terms as json feeds. Node Types can be edited in the below page of the cms.
Service Views can be edited in the below page of the cms.
Individual JSON Service views created in the cms.
Importing/Exporting Content To export content from a staging to production environment and vice versa please use the content import/export module. In the drupal shortcuts menu there are two links 1 for export content and 1 for import content. To export just click the export content and it will prompt to save a txt file of the content. To import this content click the import content and you will see a form with some options. Select the export file as the file to import and then select the parameters of the import for which content types to import etc.
Database Migrations The database of drupal is very complex so data migrations can be a real pain. The solution at the moment is to do the following steps. This is currently in the process of being automated. 1. 2. 3. 4. 5. Export the content from the admin panel. Make a copy of the database. Export your current database. Import your database on the server. Import the content that was exported in step 1.
Backend The backend is a simple mvc structure which has a lot of helper classes which allow you to easily get request information and to mock requests for unit testing. The php folder is where all the controllers and business logic should be stored. Please refer to previous projects on the structure of the controllers and models. The webapp folder is the web root of the website. No php files are needed here other than the files which are currently in the existing structure. The WEB-INF/views is the smarty views folder. All smarty views should be stored in this location. Config The configuration of the website is controlled by WEB-INF/config.ini This configuration should have all of the settings which are needed to control the application and which change between environments. This file has references to css and javascript which will be changed in the final deployment through the maven configuration. This happens by updating the pom.xml where the plugin maven-minify-plugin is setup. This plugin needs to reference the same css and js as referenced in the config.ini. This will enabled the css and js to be merged and minified in the deployment process. Please make sure to look at the overriding configuration which is stored in assembly/config.ini. This overriding config.ini should be the same as the WEB-INF/config.ini except with dynamic variables to change settings between the different environments. These settings come from the pom.xml when processing the config.ini, see below snippet for pom.xml configuration. <plugin> <groupid>net.rumati.maven.plugins</groupid> <artifactid>velocity-maven-plugin</artifactid> <version>0.1.2</version> <executions> <execution> <id>config-override</id> <phase>prepare-package</phase> <goals> <goal>velocity</goal> </goals> <configuration> <template>${basedir}/src/main/assembly/config.ini</template> <outputfile>${basedir}/target/auto-generation/webapp/web-inf/config.ini</outputfile> <properties>... <googleanalyticsid>${googleanalyticsid}</googleanalyticsid> <buildfinalname>${buildfinalname}</buildfinalname> <publiccontentlocation>${publiccontentlocation}</publiccontentlocation>... </properties> </configuration> </execution> </executions> </plugin>
Unit Testing All php code should be developed to be easily mockable and testable. This should use interfaces for model services so a mocked version and an implementation can be created. There should be no direct references to $_SERVER variables, the HttpServletRequest object in the facade should be used to pull out variables such as server name and request parameters etc. This allows controllers and models to be built and tested without the need for running through a web server. All unit tests should be stored under test/php, please refer to previous projects for references. Routing All routes should be defined in.htaccess file. Any dynamic parts of the url structure should be passed through to the controller as redirect parameters. This allows the controller to be completely separated from the route so there are no dependencies on url structures etc. An example of a route which does this in hahn is below. You will see in this example we have api/modules/{service_method}.json as the route. The Service method is the dynamic parameter to pass to the controller so that is defined by using the redirect parameter E=ServiceMethod:$1 RewriteRule ^api/modules/([^/\.]+)\.json$ view.requesthandler.php [E=ExecuteController:APIModulesCommand,E=ServiceMethod:$1,L] This is then picked up by the controller using the following code. $servicemethod = $request->getredirectparam("servicemethod"); Data binding Data binding throughout the application uses annotations which are parsed through an annotation parser and used to bind data between the frontend requests, drupal and traction. These can be seen by the below example from an entity class. You will notice annotations for DrupalField and TracField. In the drupal annotation parser it will look for properties with DrupalField and if found will bind the node object field which is retrieved from drupal to the property in the entity class. In the traction sdk it will look for properties with TracField and if found will add that property into a request to send to traction.
/** * @Entity class ContactEntity{ /** * @DrupalField(name="nid",customfield=false) * @var integer $nodeid public $nodeid; /** * Special field used for when inserting into traction. * Response object id will be populated * @TracCustomerId * @var integer $id public $tractioncustomerid; /** * @DrupalField * @TracField(name="FIRSTNAME") * @var string $firstname public $firstname = null; /** * @DrupalField * @TracField(name="LASTNAME") * @var string $lastname public $lastname = null; } If you are doing html form binding this can be done by using the below annotations to bind fields to specific data types automatically. You will notice BindType annotation is used here. See RequestBindUtils::bindToRequest for supported binding types.
/** * @Entity class NominationEntity{ /** * @var string $story_description public $story_description = null; /** * @var string $friend_email public $friend_email = null; /** * @BindType(type="datetime") * @var DateTime $invitation_date public $invitation_date = null; /** * @BindType(type="boolean") * @var bool $optin_nominee public $optin_nominee = false; } A controller which handles this bind can be seen with the code snippet below. // create bind model $bindmodel = new WinnerFormVO(); RequestBindUtils::bindToRequest($bindModel, $request, array("winner_country","winner_state","winner_postcode","agreeterms","optin"), null); Data validation Data validation uses a simple validation api with a lot of helper methods. See ValidationUtils for supported methods. An example use of a validator class snippet is below.
class NominationWinnerValidator{ public function construct(){ } /** * Validates model parameters * @param NominationEntity $model * @param HttpServletRequest $request * * @return Errors public function validate($model, $request){ $errors = new Errors(); ValidationUtils::rejectIfNullOrEmpty($errors, "winner_firstname", $model->winner_firstname); ValidationUtils::rejectIfMax($errors, "winner_firstname", $model->winner_firstname, 50); ValidationUtils::rejectIfNotPattern($errors, "winner_firstname", $model->winner_firstname, "/^[^\\d]+$/"); return $errors; } } Frontend The frontend uses standard html markup running through smarty templates. All templates should be named with.tpl extension. Templates should not contain any php code. If data needs to be parsed to the templates that should come from the model object that the controllers pass to smarty. Static file references should always use the model attribute called {$cdnresourcespath}. This allows all css, js, images etc anything which is sitting under the webapp/resources path to be referenced. This allows for a lot of flexibility if needing to store files on a cdn or updating the path references for deployments. For example: If you are referencing a css file in resources/css/file1.css and the application is already deployed then when you make updates to this file you want updates to be rendered immediately without clients clearing their cache. By adding {$cdnresourcespath} infront of the file to look like this {$cdnresourcespath}/css/file1.css then the deployed file will look like http://do main.com/static/123454654/css/file1.css. This makes deployments extremely simple since all static resources are time stamped for each deployment so references to them are updated immediately and no references to the updated files need to be changed in html.
Javascript Libraries All javascript files should be added in the webapp/resources path under js folder. To import the javascript file into the html do not create a script reference in the html! You will need to add the reference in the webapp/web-inf/config.ini and the pom.xml. This is because the config.ini will be used to retrieve the list of javascript files and the pom.xml will be used to minify those javascript libraries into a single js file. Please refer to the below snippets from a previous project. Config.ini... desktop_view_js[] = "/frontend/js/lib/jquery/jquery-1.7.2.js" desktop_view_js[] = "/frontend/js/lib/jquery/jquery-class.js"... pom.xml... <jssourcefile>lib/jquery/jquery-1.7.2.js</jssourcefile> <jssourcefile>lib/jquery/jquery-class.js</jssourcefile>... Css Stylesheets All css files should be added in the webapp/resources path under css folder. To import the css file into the html do not create a link reference in the html! You will need to add the reference in the webapp/web-inf/config.ini and the pom.xml. This is because the config.ini will be used to retrieve the list of css files and the pom.xml will be used to minify those css files into a single css file. Please refer to the below snippets from a previous project. Config.ini... desktop_view_css[] = "/frontend/css/core.css" desktop_view_css[] = "/frontend/css/main.css"... pom.xml
... <csssourcefile>core.css</csssourcefile> <csssourcefile>main.css</csssourcefile>... Messages All copy in the frontend should come from the messages.ini file unless coming from the cms. This allows for translations to be done on the copy in the website or the copy can be migrated to the cms at a later stage. The messages file is a standard php ini file, use below references for example. To retrieve a message in smarty templates call the message like the following. <p>{$messages["messagesgroupname"]["messagekey"]}</p> For example if your messages file looks like this. [messages] mymessagekey="lorem ipsum" Then you would call this message like so. <p>{$messages["messages"]["mymessagekey"]}</p> <!-- Output would be <p>lorem ipsum</p> --> Common Smarty variables from controllers The table below is a list of variables which can be accessed from smarty templates in the existing projects. This can be used as a reference to what variables you can use. These variables are inserted from the php controller by calling SupportUtils::addDefaultModelVariables($model, $facade). Variable Description Type {$config} The config ini object Array {$messages} The messages ini object Array {$browser} {$ismobile} The browser object to retrieve user agent data The is mobile boolean to determine if user agent is mobile. This is short syntax for calling browser->ismobile() Browser Boolean
{$istablet} {$request} {$webrootpath} {$cdnrootpath} {$resourcespath} {$cdnresourcespath} {$contentpath} {$cdncontentpath} The is tablet boolean to determine if user agent is tablet. This is short syntax for calling browser->istablet() The http request object. This can be referenced if requiring to check for server params etc. The absolute path to web root. Use this for any files which need to be reference from the root of the website The absolute path to the cdn root. Use this for any files which are static and referenced from the root of the cdn. If cdn paths are not defined this defaults back to webrootpath The absolute path to the static resources from the root of the website. This should only be used if your wanting to reference files on the root of the website otherwise cdnresourcespath should be used. This path references files under webapp/resources. So if you have a files in webapp/resources/js/file1.js you would reference it by {$resourcespath}/js/file1.js The absolute path to the static resources from the root of the cdn. This path references files under webapp/resources. So if you have a files in webapp/resources/js/file1.js you would reference it by {$cdnresourcespath}/js/file1.js The absolute path to the static content from the root of the website. This should only be used if your wanting to reference files on the root of the website otherwise cdncontentpath should be used. This path references files under the content alias for cms uploaded content. The absolute path to the static content from the root of the cdn. This path references files under the content alias for cms uploaded content. Boolean HttpServletRequest String String String String String String {$metadata} The current metadata object for the current page. MetadataVO
Variable references from Javascript Sometimes you may want to get absolute path references in javascript files. These can be referenced by a window variable MainConfig. This config object is inserted into the html page by the php controller calling SupportUtils::getHtmlConfig($model, $facade). Here is an example of how to get the cdn resources path from javascript. var imagepath = window.mainconfig["cdnresourcespath"] + "/images/myimage.jpg"; Deployment The deployment process of the site happens with a maven packaging command which generates a deployable zip file that can then be extracted to the server. Most projects should be running through Bamboo build server which automatically runs the maven packaging commands and deploys to the servers. If the project is setup in Bamboo all which is required is to adhere to the documentation on frontend and backend in this document. Once you have completed development of a feature or bug fix etc commit all your code into git and push to the origin server. This will then trigger an update in Bamboo and the code will be automatically deployed to the staging server within about 10 minutes. If you and the producer are then happy with the staging deployment ask your Tech Lead to click the deploy to live in Bamboo. What is included in the deployments Currently only files in source control are deployed to the different servers. A solution is being worked on for content deployments, so if you need to migrate content from staging to live and vice versa please use the import/ export module. Packaging with maven (if required) If it is required that you have to package up the project with maven to do a manual deployment please following the following steps. Browse to the root of you project. Package for staging server. mvn clean mvn -Pstaging package Package for live server. mvn clean mvn package
Creating a tag release and version increment Once you do a live production deployment it is good practice to create a tag in the source control and increment the version of the project. You can do this using the maven release plugin by following the below steps. This will ask you a series of questions which you can hit enter on all of them except if you want to change the version names. Browse to the root of you project. Create release mvn clean mvn release:prepare mvn release:perform mvn release:clean This would have created a tag in the repository and increased the version in the maven pom.xml files. We then want to push up the changes to the origin. git push origin --tags git push origin master Training resources Drupal: Drupal Essential Training: http://www.lynda.com/drupal-7-tutorials/essential-training/73655-2.html Drupal Advanced Training: http://www.lynda.com/course-tutorials/drupal-7-advanced-training/97405-2.html Responsive Design with Drupal: http://www.lynda.com/drupal-tutorials/responsive-design-drupal/107419-2.html Services Module used to pass the data from CMS to frontend: https://drupal.org/project/services Smarty: Crash course: http://www.smarty.net/crash_course Best practices: http://www.smarty.net/best_practices PHP: PHP ini: http://php.net/manual/en/function.parse-ini-file.php