CampaignChain Documentation

Developers

Get started with CampaignChain quickly in this step-by-step guide.

Quick Tour for Developers

Learn how to start developing with CampaignChain. This page gives you some pointers to the most important concepts behind CampaignChain, how to install it and a tutorial that explains how to create your first modules to connect to an online channel.

Installation

Install CampaignChain with some sample data in 10 minutes! Learn all about the system requirements, step-by-step installation and configuration as well as how to load the sample data in the Community Edition installation tutorial.

Architecture

Before you start developing with CampaignChain, please make yourself familiar with the following concepts of its software architecture:

  1. Features, entities, calls to action
  2. General introduction to modules

Development

Customizing and enhancing CampaignChain can all be done through modules. There is an in-depth tutorial available that shows you how to Connect a new Online Channel.

Learn the general concepts of developing with CampaignChain.

The Developer Book

What is CampaignChain?

CampaignChain is open-source campaign management software to plan, execute and monitor digital marketing campaigns across multiple online communication channels, such as Twitter, Facebook, Google Analytics or third-party CMS, e-commerce and CRM tools.

For developers, CampaignChain is a platform to integrate key marketing campaign management functions with data from multiple channels. It is implemented in PHP on top of the Symfony framework.

Key Features

CampaignChain covers three main areas of outbound and inbound campaign management:

Planning

  • Define campaign goals and milestones.
  • Create and schedule campaign activities and operations on multiple online channels.
  • View and modify campaign activities and operations using an interactive timeline.

Execution

  • Automatically execute scheduled activities and operations.
  • Collect data for monitoring during campaign duration.
  • Automatically notify responsible persons if errors occur during campaign execution.

Monitoring

  • Analytics reports: Channel-specific reporting and analytics (number of Facebook views and comments, number of Twitter retweets, and so on) for accurate campaign ROI measurement.
  • Budget reports: Defining budgets and spend per channel.
  • Sales reports: Integrate with CRM and other tools to view and analyze leads generated by each campaign.
Basic Concepts

CampaignChain’s software architecture has been designed along digital marketing terms and concepts in a specialized way, so this section gets you up to speed on CampaignChain’s terminology and explains the main entities to you.

CampaignChain knows two types of entities, a Medium and an Action, which are:

Medium Action
  • Channel
  • Location
  • Campaign
  • Milestone
  • Activity
  • Operation
Campaigns

Campaigns are at the core of CampaignChain, and are the “DNA of modern digital marketing”[1]. In CampaignChain, every campaign uses one or more communication channels. Campaigns also have milestones and activities.

Campaigns usually come in two variants: manually scheduled campaigns, which have a defined start and end date, and triggered campaigns (also called nurtured campaigns), which occur in response to user events. A campaign focused on a new product launch is an example of the former, whereas a drip email campaign that begins when a user fills up a registration form is an example of the latter.

Channels & Locations

Campaigns use online channels, which are the pathways by which campaign content reaches its audience. Common examples of channels include websites, blogs and social networks like Facebook and LinkedIn. For monitoring purposes, CampaignChain also allows connections to channels to retrieve traffic statistics (e.g. Google or YouTube Analytics) and lead generation data maintained in a CRM.

Every channel includes one or more locations, which allow granular publishing of campaign content. For example, a Twitter channel has only one location: the Twitter stream. However, a website channel might have various locations: a landing page, a banner on the home page, a “Contact Us” page with a form, and so on. Similarly, a LinkedIn channel might consist of two locations: a company profile page and a news stream. Locations are being created when connecting to a new Channel.

Furthermore, Locations can be created by an Operation. For example, an Operation that posts a Tweet on a Twitter stream is essentially creating a new Location (i.e. that Tweet) within a Location (i.e. a Twitter user’s stream). Learn more about Operations below.

Milestones

Milestones are key events or reference points during a campaign. For example, the campaign go-live date could be a milestone, and a press tour could be a second milestone. When you set up campaign milestones, related actions can be defined. For example, you could compare analytics data between two milestones. Or you could notify a member of your marketing team to start working on the next set of tasks once a milestone has been reached.

Activities and Operations

Every location allows one or more activities which can be undertaken. For example, creating a new post is an example of an activity for a blog channel.

Every activity must always have at least one operation. For example, posting on Twitter is one activity which equals the operation.

In other cases, a single activity may encompass multiple operations. For example, defining and creating a Google AdWords campaign that runs for 3 months is a possible activity for the Google AdWords channel. However, this activity could consist of two operations: the first operation might be a Google Ad that runs for the first 2 months of the campaign, and the second operation would be a second, different Google Ad that runs for the remaining 4 weeks.

Operations and Locations

Locations are created when connecting to a new Channel or by an Operation. Upon creation by a Channel, the URL of the Location is usually known and can be stored in the system when creating the new Location. For example, when connecting a new Twitter user stream to CampaignChain, the user’s URL on Twitter will be accessible (e.g. www.twitter.com/ordnas).

This is different when it comes to Operations. An Operation could well create a Location stub without the URL and only provide the URL after the Operation has been executed. For example, the URL of a scheduled tweet will only be generated by Twitter once the tweet has been posted. Hence, CampaignChain allows Operations to create Locations without a URL, but requires them to provide a URL when the Operation gets executed.

Visual Summary

The following diagram explains the relationship between the various entities.

_images/components-conceptual.png

It should be clear from this diagram an Activity is never related directly to a Channel. The relationship is always Channel -> Location -> Activity -> Operation.

A more concrete example of this relationship is illustrated below.

_images/components-realized.png
Modules and Hooks

CampaignChain has been designed so that it does not require you to replace existing digital marketing applications. Instead, it serves as a platform for integrating such applications and acts as a cockpit for managing digital marketing campaigns.

Modules

Due to CampaignChain’s modules architecture, any online channel along with its locations can be integrated. Furthermore, custom campaigns, milestones, activities and operations can be developed. Given that CampaignChain is built on top of the Symfony framework, modules can use functionality provided by other modules.

Hooks

Hooks are reusable components that provide common functionality and can be used across modules to configure campaigns, milestones, channels, locations, activities and operations. CampaignChain already provides a number of hooks and developers can easily add new ones.

For example, CampaignChain comes with an assignee hook, which makes it possible to assign specific channels or activities to members of a marketing team. Similarly, CampaignChain’s due date hook can be used to specify a due date for a Twitter post activity; the same hook can be reused to define a due date for a campaign milestone.

Call to Action

CampaignChain allows tracking Calls to Action across various Channels and Locations to understand which Operations had the highest impact. Imagine the following conversion funnel:

  1. A Twitter post links to a landing page on a website.
  2. The landing page includes a registration form to download something.
  3. All the personal data collected in the form will be saved as leads in a CRM.

With CampaignChain, you will be able to understand how many leads have been generated by that specific Twitter post.

Learn more about the details of CampaignChain’s Call to Action (CTA) Tracking.

User Interface

CampaignChain’s Web-based user interface has been implemented with Bootstrap 3. Thus, it is responsive and works on desktop computers as well as mobile devices such as tablets and smartphones.

Footnotes
[1]This terminology was used by Lars Trieloff in his Feb 2014 presentation, which also inspires CampaignChain’s architecture.

Development Mode

It is highly recommended that you configure CampaignChain to run in development mode while you work on the code.

To enable development mode, set the campaignchain_dev``parameter to ``true in app/config/parameters.yml.

You should only do this prior to a fresh installation of CampaignChain and not switch back to production mode for that installation.

When in development mode, the following happens:

require-dev in composer.json

While in development mode, CampaignChain will upon installation also register CampaignChain modules which have been defined in the require-dev section of your project’s composer.json file.

Development Tools

The navigation bar will display an icon to access various developer tools from within the CampaignChain user interface.

_images/dev_tools.png
Modules Repositories

You can specify modules repositories that should be used in development mode instead of the ones you defined for the production instance.

The development modules repositories can be defined in the campaignchain.yml of a distribution module.

modules:
    repositories:
        - http://www.example.com/modules/
    repositories-dev:
        - http://www.example.com/modules/dev/

In case no development modules repositories have been defined under repositories-dev, CampaignChain will fall back and use the ones specified under repositories.

Module Basics

CampaignChain uses a modular architecture, allowing developers to integrate any online channel along with its locations, activities and operations.

This document provides an overview of common concepts that developers should know when developing CampaignChain modules, e.g. for custom channels, locations, activities and operations.

Framework
Symfony

CampaignChain has been built on top of the PHP-based Symfony framework. Therefore, custom modules should also be developed with Symfony.

Doctrine

Within Symfony, CampaignChain uses Doctrine as its Object-Relation Mapper (ORM). This allows usage of various databases in the back-end.

Bootstrap 3

CampaignChain’s GUI is based on Bootstrap 3 and module developers should follow its best practices of responsive design.

Types of Modules

CampaignChain’s core can be extended through various types of modules, each covering a certain feature set. The following pre-defined types exist:

  • Activity, e.g. post on Facebook or Twitter.
  • Campaign, to develop custom campaign functionality (e.g. nurtured campaigns).
  • Channel, to connect to channels such as Facebook or Twitter.
  • Location, to manage e.g. various Facebook pages.
  • Milestone, e.g. to develop a new kind of milestone besides the default one with a due date.
  • Operation, similar to Activity module type.
  • Report, to create custom analytics, budget or sales reports for ROI monitoring.
  • Security, e.g. functionality for channels to log in to third-party systems.
  • Distribution, an aggregation of bundles and system-wide configuration, e.g. the CampaignChain Community Edition.

The concepts in this document apply to all types of modules.

Packaging

CampaignChain modules are developed as Symfony bundles. One Symfony bundle must contain at least one CampaignChain module and can contain various CampaignChain modules of the same type.

To allow CampaignChain to install a bundle along with its module(s), the bundle must contain the following two configuration files in its root:

  • composer.json: CampaignChain modules (residing inside a Symfony bundle), are installed/distributed as Composer packages. This file holds information relevant to Composer.
  • campaignchain.yml: This file holds all the CampaignChain-specific module configuration parameters.

Since CampaignChain is built on top of the Symfony framework, modules can use functionality provided by other modules mainly through Symfony services.

Versioning

The version number of module packages for CampaignChain must follow the syntax laid out in the Semantic Versioning specification.

Bundle Generation

When building an CampaignChain module, the first step is to create a new Symfony bundle.

Configuration Files

Every bundle with CampaignChain modules must have the following two configuration files, which are essential for CampaignChain to correctly identify and integrate the included module(s).

These files must be located in the root of the bundle directory.

composer.json

The bundle’s composer.json file follows standard Composer conventions. The type parameter must belong to the set of pre-defined module types as outlined previously. Here is a list of parameters typically seen in this file:

  • require: A list of package dependencies
  • description: A human-readable description for the bundle
  • keywords: Additional descriptive keywords for the bundle
  • homepage: A link to the bundles’s website
  • license: The license under which the bundle and its modules are made available
  • authors: A list of package authors

Example:

{
   "name": "campaignchain/channel-twitter",
   "description": "Connect with Twitter.",
   "keywords": ["twitter","oauth"],
   "type": "campaignchain-channel",
   "homepage": "http://www.groganz.com",
   "license": "Proprietary",
   "authors": [
       {
           "name": "Sandro Groganz",
           "email": "sandro@campaignchain.com"
       }
   ],
   "require": {
       "campaignchain/core": "dev-master",
       "campaignchain/security-authentication-client-oauth": "dev-master"
   }
}

In addition to the schema of the composer.json file developers of CampaignChain modules should also follow the best practices outlined below.

Parameter name

The name of the bundle. Typically this is the application name or vendor name, followed by a separating slash (/), then the module type followed by a dash and the bundle’s purpose.

The schematic representation of the syntax is: <application or vendor name>/<bundle type>-<purpose of bundle>

Example: campaignchain/channel-twitter

Parameter type

The type of the bundle, which must be one of

  • campaignchain-channel
  • campaignchain-location
  • campaignchain-activity
  • campaignchain-operation
  • campaignchain-report
  • campaignchain-campaign
  • campaignchain-security
  • campaignchain-milestone

Custom types are not supported and CampaignChain will display an error if it encounters a type value outside the above allowed set.

Other Parameters Required by CampaignChain
  • description: A human-readable description for the bundle
  • keywords: Additional descriptive keywords for the bundle
  • homepage: A link to the bundles’s website
  • license: The license under which the bundle and its modules are made available
  • authors: A list of package authors
campaignchain.yml

The bundle’s campaignchain.yml file specifies all CampaignChain modules contained in the bundle. Per module, it defines parameters such as the internal name of the module, used to reference it from other modules, as well as any associated Symfony routes and Symfony services or CampaignChain hooks. The information in the file varies depending on the module type and requirements.

The typical structure of the campaignchain.yml file is as follows:

modules:
    |module-identifier|:
        display_name: |display name|
        channels:
            - |channel identifier|
            - |channel identifier|
            ...
        services:
            - job: |service identifier|
        routes:
            - new: |route identifier|
            - edit: |route identifier|
            - edit_modal: |route identifier|
            - edit_api: |route identifier|
            - read: |route identifier|
        hooks:
            - |hook-name|: |true|false|
            - |hook-name|: |true|false|
            ...
        system:
            navigation:
                settings:
                    - [|Nav item name|, |symfony_route|]
                    ...
                ...
    |module-identifier|:
        ...

Example: An activity module’s campaignchain.yml file lists the channels the activity belongs to and the Symfony routes to create and edit new activities.

modules:
   campaignchain-twitter-update-status:
       display_name: 'Update Status'
       channels:
           - campaignchain/channel-twitter/campaignchain-twitter
       services:
           job: campaignchain.activity.twitter.job.update_status
       routes:
           new: campaignchain_activity_twitter_update_status_new
           edit: campaignchain_activity_twitter_update_status_edit
           edit_modal: campaignchain_activity_twitter_update_status_edit_modal
           edit_api: campaignchain_activity_twitter_update_status_edit_api
       hooks:
           campaignchain-due: true
           campaignchain-duration: false
           campaignchain-assignee: true
Module Identifier

The module’s identifier should be provided as the child of the modules parameter. Multiple modules can be specified in this way. The recommended syntax of the module identifier is to use dashes (-) to separate words, which helps to separate it from the parameters which use underscores. Furthermore, the identifier should start with an application or vendor name followed by a string that best captures the purpose of the module.

In sum, the recommended syntax is: <application or vendor name>-<purpose of module>

Example: campaignchain-twitter-update-status

Note

It is important to note that the module identifier must be unique per module type across bundles. In other words: In a bundle, only CampaignChain modules of the same type are allowed and the identifier of each module must be unique in all bundles containing the same type of modules.

Parameter display_name

All modules have to specify the module name that will be displayed in CampaignChain’s graphical user interface by providing a string as the value of the display_name parameter.

Parameter services

A module can define the following services to be consumed by CampaignChain.

  • job: This service will be called by CampaignChain’s scheduler to automatically execute functionality, e.g. publishing a scheduled post to Twitter.
Parameter routes

Within the campaignchain.yml configuration file, CampaignChain recognizes four types of Symfony routes.

  • new: The route to invoke when creating a new Channel, Location, Activity, Operation
  • edit: The route to invoke when editing an existing Channel, Location, Activity, Operation
  • edit_modal: The route to invoke for the pop-up view of the ‘edit’ route
  • edit_api: The route to invoke for the submit action of the ‘edit_modal’ route
  • read: The route where information can be viewed
Parameter hooks

Hooks can be assigned to a module by specifying the hook’s identifier and true to activate it or false to deactivate it. If a hook is omitted, CampaignChain will regard it as inactive.

Parameter system

This parameter allows a module to define system-wide configuration options. For example, to add a new navigation item to the settings navigation menu available in the header of CampaignChain’s graphical user interface.

Parameters Specific to a Module Type

Some module types require certain parameters in the campaignchain.yml configuration file to be defined. For example, an Activity module should list at least one related channel module. Similarly, an Operation module must define whether it creates a Location or not. You will find more detailed information in the documentation related to a specific module type.

Channel and Location Modules

This document provides an overview of concepts that developers should know when developing Channel and Location modules.

Note

Every Channel must include at least one Location.

Naming Conventions (Channel)

composer.json

  • The name parameter should be of the form [application name or vendor name]/channel-[channel name].

    Example: campaignchain/channel-twitter

  • The type parameter should be ‘campaignchain-channel’.

campaignchain.yml

  • The name of a Channel module should follow the convention [application name or vendor name]-[channel name].

    Example: campaignchain-twitter

Naming Conventions (Location)

composer.json

  • The name parameter should be of the form [application name or vendor name]/location-[channel name]-[bundle purpose].

    Example: campaignchain/location-twitter-status-update

  • The type parameter should be ‘campaignchain-location’.

campaignchain.yml

  • The name of a Location module should follow the convention [application name or vendor name]-[channel name]-[location descriptor].

    Example: campaignchain-twitter-user

Wizards and Routes

CampaignChain provides a set of “wizards” that ease integration of your module into the CampaignChain GUI. The Channel Wizard takes care of redirecting the client browser to the appropriate routes when creating or editing a channel and/or location.

Channel and Location module developers should use the Channel Wizard as a convenient way to attach and persist a new location for a channel. The Channel Wizard can be invoked from within a controller using the service identifier ‘campaignchain.core.channel.wizard’ as shown below.

<?php
// invoke and use channel wizard
$wizard = $this->get('campaignchain.core.channel.wizard');
$wizard->setName($profile->displayName);
$wizard->addLocation($location->getIdentifier(), $location);
$channel = $wizard->persist();
$wizard->end();

The route used by the Channel Wizard is obtained from the module configuration in the Channel bundle’s campaignchain.yml file:

modules:
   campaignchain-linkedin:
       display_name: LinkedIn
       routes:
           new: campaignchain_channel_linkedin_create
Location Module and Service

The Channel Wizard’s addLocation() method should be passed a Location object representing the location to be added to the channel. CampaignChain’s Location service can be used to retrieve the correct Location module, using the Location bundle name and Location module identifier. The location service is available using the identifier ‘campaignchain.core.location’.

<?php
$locationService = $this->get('campaignchain.core.location');
$locationModule = $locationService->getLocationModule(
  'campaignchain/location-linkedin', 'campaignchain-linkedin-user');

In this example, the Location service finds the Location module named ‘campaignchain-linkedin-user’ in the bundle named ‘campaignchain/location-linkedin’.

Channel Authentication

CampaignChain provides an OAuthBundle (based on HybridAuth) which can be used for OAuth-based authentication with online channels. The client can be accessed as a Symfony service using the service identifier ‘campaignchain.security.authentication.client.oauth.application’.

<?php
$oauthApp = $this->get(
  'campaignchain.security.authentication.client.oauth.application');
$application = $oauthApp->getApplication(self::RESOURCE_OWNER);

if(!$application){
   return $oauthApp->newApplicationTpl(self::RESOURCE_OWNER,
     $this->applicationInfo);
}
else {
   return $this->render(
       'CampaignChainChannelLinkedInBundle:Create:index.html.twig',
       array(
           'page_title' => 'Connect with LinkedIn',
           'app_id' => $application->getKey(),
       )
   );
}

The client’s getApplication() method retrieves any existing channel credentials (that were previously configured) from the CampaignChain database. In case no such credentials exist (such as the first time a location is created), the getApplicationTpl() method generates a Web form for the user to input the required data.

CampaignChain also provides an OAuth authentication client via the ‘campaignchain.security.authentication.client.oauth.authentication’ identifier. The client’s authenticate() method can be used to perform authentication against the remote service.

<?php
$oauth = $this->get(
  'campaignchain.security.authentication.client.oauth.authentication');
$status = $oauth->authenticate(self::RESOURCE_OWNER,
  $this->applicationInfo);

Note

CampaignChain’s OAuthBundle is an optional bundle to ease authentication with third-party services. Developers are free to implement their own authentication client, or use third-party clients as needed.

Channel Icon

Each Channel module must provide a channel icon image in PNG format with size 16x16 pixels. The image file must reside in the your-project/src/your-bundle-namespace/Resources/public/images/icons/16x16/ folder of the bundle and the image’s file name should match the descriptive string used at the end of the bundle name.

Example: The bundle named ‘campaignchain/channel-google’ would have its icon reside at your-project/src/Acme/CampaignChain/Channel/GoogleBundle/Resources/public/images/icons/16x16/google.png.

Activity and Operation Modules

This document provides an overview of concepts that developers should know when developing Activity and Operation modules.

Note

Every Activity must include at least one Operation and at least one Job.

Naming Conventions (Activity)

composer.json

  • The name parameter should be of the form [application name or vendor name]/activity-[bundle purpose].

    Example: campaignchain/activity-twitter

  • The type parameter should be ‘campaignchain-activity’.

campaignchain.yml

  • The name of an Activity module should follow the convention [application name or vendor name]-[channel name]-[bundle purpose].

    Example: campaignchain-twitter-update-status

Naming Conventions (Operation)

composer.json

  • The name parameter should be of the form [application name or vendor name]/operation-[bundle purpose].

    Example: campaignchain/operation-twitter

  • The type parameter should be ‘campaignchain-operation’.

campaignchain.yml

  • The name of an Operation module should follow the convention [application name or vendor name]-[channel name]-[operation descriptor].

    Example: campaignchain-twitter-update-status

Linking Activities and Channels

The Activity bundle’s campaignchain.yml file should contain a channels parameter, which specifies the link between the Channel and the Activity. The channels parameter should be of the form [channel bundle name]/[channel module name]

Example: ‘acme/channel-linkedin/acme-linkedin’ refers to the Channel bundle ‘acme/channel-linkedin’ and the Channel module within it named ‘acme-linkedin’.

Wizards and Routes

CampaignChain provides a set of “wizards” that ease integration of your module into the CampaignChain GUI. The Activity Wizard takes care of presenting the user with a form that lists available campaigns, channels and operations. Based on the user’s selection in the form, the Activity Wizard is able to retrieve and display the available Operations for the selected Channel and link it to the selected Campaign.

The Activity Wizard can be invoked from within a controller using the service identifier ‘campaignchain.core.activity.wizard’ as shown below.

<?php
// invoke and use activity wizard
$wizard = $this->get('campaignchain.core.activity.wizard');
$campaign = $wizard->getCampaign();
$activity = $wizard->getActivity();

The routes and display name used by the Activity Wizard are obtained from the module configuration in the Activity bundle’s campaignchain.yml file:

modules:
  campaignchain-linkedin-share-news-item:
      display_name: 'Share News'
      channels:
          - campaignchain/channel-linkedin/campaignchain-linkedin
      routes:
          new: campaignchain_activity_linkedin_share_news_item_new
          edit: campaignchain_activity_linkedin_share_news_item_edit
          edit_modal: campaignchain_activity_linkedin_share_news_item_edit_modal
          edit_api: campaignchain_activity_linkedin_share_news_item_edit_api
      hooks:
          campaignchain-due: true
Activities with a Single Operation

If an Activity has only one Operation, this should be made explicit by calling the Activity object’s setEqualsOperation() method, as shown below:

<?php
$wizard = $this->get('campaignchain.core.activity.wizard');
$activity = $wizard->getActivity();
$activity->setEqualsOperation(true);
Operation Form

When a user defines a new operation, CampaignChain renders a form with fields appropriate to that operation. This form must be included within the Operation module. The easiest way to create this form is by using Symfony’s Form component and FormBuilder interface to define a Form object, as shown below:

<?php
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ShareNewsItemOperationType extends AbstractType
{

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('message', 'text', array(
                'property_path' => 'message',
                'label' => 'Message',
                'attr' => array(
                    'placeholder' => 'Add message...',
                    'max_length' => 200
                )
            ));

        $builder
            ->add('submitUrl', 'text', array(
                'property_path' => 'linkUrl',
                'label' => 'URL of page being shared',
                'attr' => array(
                    'placeholder' => 'Add URL...',
                    'max_length' => 255
                )
            ));

        // ... and so on //

    }
}

This Form object can then be used within controller action methods to create or edit a new operation, as shown below:

<?php
$activityType = $this->get('campaignchain.core.form.type.activity');
$shareNewsItemOperation = new ShareNewsItemOperationType(
   $this->getDoctrine()->getManager(), $this->get('service_container')
);
$operationForms[] = array(
   'identifier' => self::OPERATION_IDENTIFIER,
   'form' => $shareNewsItemOperation,
   'label' => 'LinkedIn Message',
);
$activityType->setOperationForms($operationForms);

$form = $this->createForm($activityType, $activity);
$form->handleRequest($request);

if ($form->isValid()) {
      // process input
}
Operation Module and Service

The Activity object’s addOperation() method should be passed an Operation object representing the operation to be added to the activity. CampaignChain’s Operation service can be used to retrieve the correct Operation module, using the Operation bundle name and module identifier. The Operation service is available using the identifier ‘campaignchain.core.operation’.

<?php
$operationService = $this->get('campaignchain.core.operation');
$operationModule = $operationService->getOperationModule(
  'campaignchain/operation-linkedin', 'campaignchain-linkedin-share-news-item'
);

$operation = new Operation();
$operation->setName($activity->getName());
$operation->setActivity($activity);
$activity->addOperation($operation);

In this example, the Operation service finds the Operation module named ‘campaignchain-linkedin-share-news-item’ in the bundle named ‘campaignchain/operation-linkedin’.

Jobs

Every Operation module should include a Job, which actually executes the operation. This Job should implement the JobServiceInterface, which mandates an execute() method that is called when the job is executed. The Job is invoked by the CampaignChain scheduler when an Operation becomes due; it can also be invoked manually to execute an operation immediately.

Hooks

To process the hooks associated with an Activity, CampaignChain makes a Hook service available, via the service name ‘campaignchain.core.hook’. Call this service’s processHooks() method to process the hooks for an Activity, as shown below:

<?php
$hookService = $this->get('campaignchain.core.hook');
$activity = $hookService->processHooks(
  self::BUNDLE_NAME, self::MODULE_IDENTIFIER, $activity, $data
);
Operations and Locations

An Operation can create a new Location as its end result and/or it can include CTAs that point to Locations. For example, a LinkedIn news sharing Operation would create a new Location - a Linkedin status message that is directly accessible via a unique URL.

When you create a new Activity within CampaignChain, your Operation should also create a Location entry. At the time the Activity is created, the Location entry will necessarily be incomplete as the URL to the Location will not be known.

Once the Operation is executed, the Job that executes it must update the Location with the URL. It must also change the Location’s status from ‘STATUS_UNPUBLISHED’ to ‘STATUS_ACTIVE’.

It’s important to define the owns_location parameter in the Operation module’s campaignchain.yml file as shown below:

modules:
  campaignchain-linkedin-share-news-item:
      display_name: 'Share News'
      owns_location: true
      services:
          job: campaignchain.operation.linkedin.job.share_news_item

Call to Action (CTA) Tracking

This section describes the inner workings of CampaignChain’s Call to Action Tracking. The provided information is useful for anyone developing Operation modules with CampaignChain or for those configuring third-party channels to work with CampaignChain.

What is a CTA?

In web design, a CTA is a banner, button, or some type of graphic or text on a website meant to prompt a user to click it and continue down a conversion funnel. It is an essential part of inbound marketing as well as permission marketing in that it actively strives to convert a user into a lead and later into a customer.

Wikipedia

In CampaignChain, a CTA is essentially a URL that appears in a HTML href link or form action. It appears within an Operation and each Operation can contain 0 to many CTAs. For example, a tweet could include various links that CampaignChain treats as CTAs, while a Google Ad would contain only 1 link as the CTA.

Tracking ID

CampaignChain leverages two types of IDs for its CTA tracking:

  • CTA Tracking ID: Each CTA has a unique ID assigned by CampaignChain per URL that is included in an Operation. If you read about just the Tracking ID in the documentation, then it’s referring to the CTA Tracking ID.
  • Channel Tracking ID: For each Channel that has been connected with CampaignChain, a unique ID will be generated. This ID must be provided in the tracking code that is being included in a Channel. When the tracking code is activated, CampaignChain checks whether the provided Channel Tracking ID exists and whether the tracking code has been executed from a URL that actually resides within the Channel.
Tracking Process

To make CTA tracking work, a Channel that is connected with CampaignChain provides the relevant information to CampaignChain for tracking the CTA path that is part of a conversion funnel.

High-level View

From a 30,000-feet perspective, this is how the tracking process works:

  1. A link or form inside an Operation acts as the initial CTA at the beginning of a marketing funnel. The initial CTA contains a unique Tracking ID which allows CampaignChain to trace back the link to the respective Actions and Media (Campaign, Activity, Operation, Location, etc.). For example, a Twitter post that links to a landing page within a website.
  2. All subsequent Locations inside a connected Channel ping CampaignChain and let it know through the Tracking ID that the respective Location was referred by a CTA.

The communication between CampaignChain and a Channel is achieved through JavaScript included in the Channel that posts the CTA information to CampaignChain and optionally also through REST-based communication between CampaignChain and the Channel. Depending on the depth of integration between CampaignChain and a Channel, there are 3 different types of tracking (described in subsequent sections).

In-depth Flow Description

The single steps of the CTA tracking process are as follows:

1. The Operation’s content gets parsed for links right before execution.

2. If the Operation contains 1 or more links, the following happens:

2.1. A unique Tracking ID gets assigned to each URL, no matter if an Operation contains the same URLs multiple times.

Note

An Operation could well contain the same URL multiple times, For example, a banner image on a landing page could point to the same URL in its key visual as well as a button that is part of the banner. When analyzing the effectiveness of the banner image, you want to know whether the key visual or the button caused more clicks. That’s why each of them gets treated as a unique CTA with its own Tracking ID, although they have the same URL.

2.2. The Tracking ID gets appended to the URLs found inside the Operation.

2.2.1. If it is a full URL, then append the Tracking ID and replace the original URL with a shortened URL (CampaignChain uses Bit.ly by default).

2.2.2. If the URL is already shortened, expand it, append the Tracking ID and replace the original shortened URL with a new shortened URL.

2.3. For each link, an entry is made in the CTA table with the Tracking ID and the related Operation as well as the original URL (full and short, if the latter was provided).

3. CampaignChain executes the Operation that now contains the new short URLs with the Tracking ID, e.g. it publishes a status update on Twitter that contains a link to a landing page.

4. When someone activates a CTA, e.g. clicks a link in a Tweet published by CampaignChain, the URL points to a Location. If that Location is part of a Channel that includes the tracking code and is connected with CampaignChain, then the following happens:

4.1. The tracking code checks whether the URL that pointed to the current page includes the Tracking ID. If yes, then it proceeds. If not, then it exits.

4.2. If the Tracking ID exists, the Tracking code sends this information to CampaignChain: Channel Tracking ID, CTA Tracking ID, URL of current Location, URL of target Location and additional information useful for monitoring.

4.3. CampaignChain checks whether the Channel Tracking ID is valid, i.e. if the Channel sending the tracking data is actually connected with CampaignChain.

4.3.1. If yes, then it performs some validity checks on the data, most notably whether the Tracking ID exists within CampaignChain, and finally saves the tracking data for monitoring purposes.

4.3.2. If no, then it will not save the data and instead notify the admin of an error (most likely, the Tracking Code has been included in a Channel that has not been connected with CampaignChain yet or this is a Denial of Service attack).

4.4. While CampaignChain processes the tracking data, the tracking code in the Channel appends the Tracking ID to the target URL (if another one does not exist yet, because the target URL is part of a new Operation) or saves it in a cookie. It then redirects the visitor to the target Location.

Note

Passing on the Tracking ID enables CampaignChain to do two things:

  • Understand, whether e.g. the visitor browses a website away from a landing page before coming back to it and activating a CTA that leads to another Operation.
  • Track the effectiveness of Operations across Channels.
Types of Tracking

To track CTAs, different types of tracking are used with CampaignChain to monitor the inbound marketing funnel.

CampaignChain-to-Channel (unidirectional)
  • Integration level: Useful if CTA is under control, but not the Channel.
  • Example: We can add a Tracking ID to a link that will be published on Twitter, but we cannot install something on Twitter to establish a connection between Twitter and CampaignChain to exchange information.
  • Tracking ID: The Tracking ID must be included in the CTA. It is important, because it helps to distinguish between Campaigns and Activities if e.g. the same Landing Page is being used as a CTA target within the same Campaign various times or in different campaigns.
  • Pros: Simple to implement by adding the Tracking ID to the URL of the CTA.
  • Cons: Ideally, CampaignChain would be in control of the Operation (e.g. posting to Twitter from within CampaignChain). If not possible, then a user would have to manually append the Tracking ID.
Channel-to-CampaignChain (unidirectional)
  • Integration level: The channel sends information about the Operation, Location and CTA to CampaignChain.
  • Example: A JavaScript snippet included in Wordpress sends information to CampaignChain about a link’s URL that was clicked inside a blog post, as well as the URL of the blog entry, etc.
  • Tracking ID: At least the Tracking ID of the initial CTA should be available. Then CampaignChain is able to match the CTA’s URL provided by the Channel with the Campaign and Activity it belongs to. Information about the source and target Location is also provided by the Channel for CampaignChain to easily map the related URLs to the Locations inside CampaignChain.
  • Pros: This approach has the security advantage that the third-party application is in control of the communication towards CampaignChain.
  • Cons: There must be a mechanism inside the Channel that ensures that at least the Tracking ID of the initial CTA is being carried on to the target Location.
CampaignChain-to-Channel (bidirectional)
  • Integration level: CampaignChain and the Channel are tightly integrated when it comes to creating Operations and Locations, thus providing maximum communication between the two when it comes to CTA tracking.
  • Example: A landing page has been created within Wordpress. With CampaignChain connected to Wordpress (e.g. through a REST API), CampaignChain grabs the content of the Wordpress page, parses it, stores the CTAs the page includes and makes the page public at the scheduled time. As in the unidirectional Channel-to-CampaignChain approach, a JavaScript snippet inside Wordpress sends information to CampaignChain once the CTA gets activated.
  • Tracking ID: CampaignChain can pass all Tracking IDs for the CTAs in a Location to the Channel to be appended to each respective URL inside a Location for more granular tracking.
  • Pros: The tighter coupling allows for more granular tracking, i.e. it is possible for CampaignChain to identify not just a Location, but also the Operation that includes a triggered CTA. Also, this approach has the performance advantage that the Channel as well as CampaignChain can handle the tracking more efficiently, because both are aware of all relevant information.
  • Cons: Creating the tighter integration requires a higher investment in terms of time and money.

Hands-on tutorials that teach you how to use CampaignChain in concrete scenarios.

The Developer Cookbook

Connect A New Online Channel

The tutorial will walk you through the process of adding support for a new online channel - in this case, LinkedIn - to CampaignChain by creating and programming the necessary modules and packaging them into a Symfony bundle.

The new LinkedIn bundle will include functionality to connect to a user’s LinkedIn activity stream and post updates to it.

Assumptions and Prerequisites
Connect Channels and Locations
1. Generate Channel and Location bundles

The first step is to create a bundle for the LinkedIn channel. In this case, we’ll assume the organization name is Acme, and use this organization name for the module namespaces.

The following commands walk you through the process. Note that you’re safe using Symfony’s defaults for all interactive prompts except for certain items shown below

$ php app/console generate:bundle
Bundle namespace: Acme/CampaignChain/Channel/LinkedInBundle
Configuration format (yml, xml, php, or annotation): yml
Do you want to generate the whole directory structure [no]? yes

Symfony will now produce a new bundle containing stub code and files, in the location your-project/src/Acme/CampaignChain/Channel/LinkedInBundle. The name of the bundle will be AcmeCampaignChainChannelLinkedInBundle.

Follow the steps above to generate a similar bundle for a location in the channel. In this tutorial, the location will be the user’s LinkedIn activity stream. When creating this bundle, you will specify the bundle namespace as Acme/CampaignChain/Location/LinkedInBundle. The new bundle will be created at your-project/src/Acme/CampaignChain/Channel/LinkedInBundle with the name AcmeCampaignChainLocationLinkedInBundle.

2. Create Configuration Files

Every CampaignChain bundle needs two configuration files: composer.json and campaignchain.yml. So the next step is to create these configuration files for your Channel and Location bundles.

To begin, create the composer.json file for the Channel bundle:

 // src/Acme/CampaignChain/Channel/LinkedInBundle/composer.json
 {
   "name": "acme/channel-linkedin",
   "description": "Connect with LinkedIn",
   "keywords": ["linkedin","oauth"],
   "type": "campaignchain-channel",
   "homepage": "http://example.ac.me",
   "license": "Proprietary",
   "authors": [
       {
           "name": "John Doe",
           "email": "john@example.ac.me"
       }
   ],
   "require": {
       "campaignchain/core": "dev-master",
       "campaignchain/security-authentication-client-oauth": "dev-master"
   }
}

The important point to note here is the type parameter, which specifies the bundle type as ‘campaignchain-channel’.

You should also create a composer.json for the Location bundle, as shown below:

// src/Acme/CampaignChain/Location/LinkedInBundle/composer.json
{
   "name": "acme/location-linkedin",
   "description": "LinkedIn user stream.",
   "keywords": ["linkedin", "user", "stream"],
   "type": "campaignchain-location",
   "homepage": "http://example.ac.me",
   "license": "Proprietary",
   "authors": [
       {
           "name": "John Doe",
           "email": "john@example.ac.me"
       }
   ],
   "require": {
       "acme/channel-linkedin": "dev-master"
   }
}

Notice again that the type parameter reflects the new bundle type - in this case, ‘campaignchain-location’ - and the require parameter specifies a dependency on the previous Channel bundle, by using the name defined in the Channel bundle’s composer.json file.

In addition to composer.json, every CampaignChain bundle must also include an campaignchain.yml file, which CampaignChain uses to correctly wire the module(s) in the bundle into the system.

Note

Although an CampaignChain bundle can contain multiple modules, we’ll keep things simple in this tutorial and assume that each bundle contains only a single module.

Your next step is to define the Channel bundle’s campaignchain.yml file, which specifies the display name for the LinkedIn Channel module, any routes used by the module and any hooks. Here’s what the file looks like:

# src/Acme/CampaignChain/Channel/LinkedInBundle/campaignchain.yml

modules:
  acme-linkedin:
    display_name: LinkedIn
    routes:
        new: acme_campaignchain_channel_linkedin_create
    hooks:
        campaignchain-assignee: true

Following CampaignChain conventions, the Channel module name includes the vendor name and a string that describes the purpose of the module - in this case, ‘acme-linkedin’. The module configuration also specifies the name of the Symfony route to be used when creating a new channel (you’ll define this route and its associated view script in the next few steps) and any hooks used by the module.

Once the Channel module is defined, the next step is to tell CampaignChain about the locations supported by the channel. This is specified via the Location bundle’s campaignchain.yml file, as shown below:

# src/Acme/CampaignChain/Location/LinkedInBundle/campaignchain.yml

modules:
  acme-linkedin-user:
    display_name: LinkedIn user stream

This configuration informs CampaignChain about the location module representing the LinkedIn user stream and specifies its display name in the CampaignChain GUI. Note that as before, the Location module name contains the vendor name and a brief descriptive string identifying the location.

3. Define Channel Routes

In general, a Channel module should take care of creating a new location and handling authentication between CampaignChain and the channel. This implies that the Channel module should define two routes: one to create a new channel (‘new’) and one to handle authentication (‘login’).

In the previous step, you specified the name for the Channel module’s ‘new’ route. Now, it’s time to follow through by actually defining the URLs for the ‘new’ route and the additional required ‘login’ route, and their respective controller actions.

To do this, update the file your-project/src/Acme/CampaignChain/Channel/LinkedInBundle/Resources/config/routing.yml as shown below:

# src/Acme/CampaignChain/Channel/LinkedInBundle/Resources/config/routing.yml

acme_campaignchain_channel_linkedin_create:
  pattern:  /channel/linkedin/create
  defaults: { _controller: AcmeCampaignChainChannelLinkedInBundle:LinkedIn:create }

acme_campaignchain_channel_linkedin_login:
  pattern:  /channel/linkedin/create/login
  defaults: { _controller: AcmeCampaignChainChannelLinkedInBundle:LinkedIn:login }

Note

You can delete the default ‘hello’ route added by the Symfony bundle generator in the above file. Similarly, you can delete the default ‘hello’ route in the Location module’s routing.xml file, which can be found at your-project/src/Acme/CampaignChain/Location/LinkedInBundle/Resources/config/routing.yml.

4. Add Controllers and Views

Next, you’ll need to create views and controllers for the routes above. First up, you’ll handle the ‘new’ route, by creating a LinkedInController with a createAction() method, as shown below.

<?php
// src/Acme/CampaignChain/Channel/LinkedInBundle/Controller/LinkedInController.php

namespace Acme\CampaignChain\Channel\LinkedInBundle\Controller;

use CampaignChain\CoreBundle\Entity\Location;
use Acme\CampaignChain\Location\LinkedInBundle\Entity\LinkedInUser;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Request;

class LinkedInController extends Controller
{
   const RESOURCE_OWNER = 'LinkedIn';

   private $applicationInfo = array(
       'key_labels' => array('key', 'App Key'),
       'secret_labels' => array('secret', 'App Secret'),
       'config_url' => 'https://www.linkedin.com/secure/developer',
       'parameters' => array(
           "force_login" => true,
       ),
   );

   public function createAction()
   {
       $oauthApp = $this->get(
         'campaignchain.security.authentication.client.oauth.application'
       );
       $application = $oauthApp->getApplication(self::RESOURCE_OWNER);

       if(!$application){
           return $oauthApp->newApplicationTpl(self::RESOURCE_OWNER,
             $this->applicationInfo);
       }
       else {
           return $this->render(
               'AcmeCampaignChainChannelLinkedInBundle:Create:index.html.twig',
               array(
                   'page_title' => 'Connect with LinkedIn',
                   'app_id' => $application->getKey(),
               )
           );
       }
   }
}

The createAction() method wraps CampaignChain’s OAuth module and renders a splash page asking the user to connect to the LinkedIn account by providing credentials and granting permission to CampaignChain to access user data. This page is rendered with the view script shown below:

# src/Acme/CampaignChain/Channel/LinkedInBundle/Resources/views/Create/index.html.twig

{% extends 'CampaignChainCoreBundle:Base:base.html.twig' %}

{% block body %}
 <div class="jumbotron">
 <p>Connect to the LinkedIn account by logging in to LinkedIn. Your username
 and password will remain with LinkedIn and will not be stored in this
 application.</p>
 <p><a class="btn btn-primary btn-lg" role="button"
 onclick="popupwindow('{{ path('acme_campaignchain_channel_linkedin_login') }}',
 '',600,600);">Connect now</a></p>
 </div>

{% endblock %}

Clicking the “Connect now” button in the above view activates the ‘login’ route defined earlier. You now need to write a corresponding controller action to use the credentials entered by the user, attempt authentication and if successful, add the location to the CampaignChain database for later use.

To simplify this task, CampaignChain provides a Location service and a Channel Wizard which together encapsulate most of the functionality you will need. The code below illustrates the typical process:

<?php
// src/Acme/CampaignChain/Channel/LinkedInBundle/Controller/LinkedInController.php

namespace Acme\CampaignChain\Channel\LinkedInBundle\Controller;

use CampaignChain\CoreBundle\Entity\Location;
use Acme\CampaignChain\Location\LinkedInBundle\Entity\LinkedInUser;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Request;

class LinkedInController extends Controller
{

   public function loginAction(Request $request){
           $oauth =
             $this->get(
               'campaignchain.security.authentication.client.oauth.authentication'
             );
           $status = $oauth->authenticate(self::RESOURCE_OWNER,
             $this->applicationInfo);
           $profile = $oauth->getProfile();

           if($status){
               try {
                   $repository = $this->getDoctrine()->getManager();
                   $repository->getConnection()->beginTransaction();

                   $wizard = $this->get('campaignchain.core.channel.wizard');
                   $wizard->setName($profile->displayName);

                   // Get the location module.
                   $locationService = $this->get('campaignchain.core.location');
                   $locationModule = $locationService->getLocationModule(
                     'acme/location-linkedin', 'acme-linkedin-user');

                   $location = new Location();
                   $location->setIdentifier($profile->identifier);
                   $location->setName($profile->displayName);
                   $location->setLocationModule($locationModule);
                   $location->setImage($profile->photoURL);
                   $location->setURL($profile->profileURL);

                   $wizard->addLocation($location->getIdentifier(), $location);

                   $channel = $wizard->persist();
                   $wizard->end();

                   $oauth->setLocation($channel->getLocations()[0]);

                   $linkedinUser = new LinkedInUser();
                   $linkedinUser->setLocation($channel->getLocations()[0]);
                   $linkedinUser->setIdentifier($profile->identifier);
                   $linkedinUser->setDisplayName($profile->displayName);
                   $linkedinUser->setProfileImageURL($profile->photoURL);
                   $linkedinUser->setProfileURL($profile->profileURL);

                   $repository->persist($linkedinUser);
                   $repository->flush();

                   $repository->getConnection()->commit();

                   $this->get('session')->getFlashBag()->add(
                       'success',
                       'The LinkedIn location <a href="#">'.
                       $profile->displayName.'</a> was connected
                       successfully.'
                   );
               } catch (\Exception $e) {
                   $repository->getConnection()->rollback();
                   throw $e;
               }
           } else {
               // A channel already exists that has been connected
               // with this Facebook account
               $this->get('session')->getFlashBag()->add(
                   'warning',
                   'A location has already been connected for this LinkedIn account.'
               );
           }

       return $this->render(
           'AcmeCampaignChainChannelLinkedInBundle:Create:login.html.twig',
           array(
               'redirect' => $this->generateUrl('campaignchain_core_channel')
           )
       );
   }
}

The first few lines of the loginAction() action method use CampaignChain’s OAuth module to authenticate against the remote service. If authentication is successful, the OAuth object’s getProfile() method returns the profile of the authenticated user. This location now needs to be added to CampaignChain’s database.

To accomplish this, the action method first creates a new Channel Wizard object, which is a convenience object that makes it easy to connect the new location to the channel and save it to CampaignChain’s database. The Channel Wizard is invoked as a Symfony service. The Channel Wizard is also assigned a name using its setName() method; this could be a fixed name, or based on input entered by the user (although you’d need to provide a form field in the view to accept this input).

Every channel must have at least one location. The action method then calls CampaignChain’s Location service to identify the Location module. The Location bundle’s name and unique module identifier play a critical role in helping the Channel Wizard correctly identify and store the location so that CampaignChain can correctly generate routes for the location.

The method initializes a new Location object using the information from the returned user profile, and attaches this Location object to the channel using the channel wizard’s addLocation() method. The information about the new location is saved to the database using the channel wizard’s persist() method.

Since every location is typically associated with a user, it makes sense to also store information about the user in the CampaignChain database. The typical properties you’d want to store are the user identifier, first name, last name, email address, profile URL and profile image URL, plus any properties specific to the channel you’re connecting.

The action method above uses a LinkedInUser object, implemented as a Doctrine entity with properties for the user identifier, first name, last name, email address, LinkedIn profile URL and LinkedIn profile image URL. The code for this LinkedInUser entity is as follows:

<?php
// src/Acme/CampaignChain/Location/LinkedInBundle/Entity/LinkedInUser.php

namespace Acme\CampaignChain\Location\LinkedInBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="acme_location_linkedin_user")
 */
class LinkedInUser
{
   /**
    * @ORM\Column(type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
   protected $id;

   /**
    * @ORM\OneToOne(targetEntity="CampaignChain\CoreBundle\Entity\Location",
    *   cascade={"persist"})
    */
   protected $location;

   /**
    * @ORM\Column(type="string", length=255, unique=true)
    */
   protected $identifier;

   /**
    * @ORM\Column(type="string", length=255, name="display_name")
    */
   protected $displayName;

   /**
    * @ORM\Column(type="string", length=255, name="first_name", nullable=true)
    */
   protected $firstName;

   /**
    * @ORM\Column(type="string", length=255, name="last_name", nullable=true)
    */
   protected $lastName;

   /**
    * @ORM\Column(type="string", length=255, nullable=true)
    */
   protected $email;

   /**
    * @ORM\Column(type="string", length=255, name="profile_url", nullable=true)
    */
   protected $profileURL;

   /**
    * @ORM\Column(type="string", length=255, name="profile_image_url",
    *   nullable=true)
    */
   protected $profileImageURL;


   /**
    * Get id
    *
    * @return integer
    */
   public function getId()
   {
       return $this->id;
   }


   /**
    * Set identifier
    *
    * @param string $identifier
    * @return LinkedInUser
    */
   public function setIdentifier($identifier)
   {
       $this->identifier = $identifier;

       return $this;
   }

   /**
    * Get identifier
    *
    * @return string
    */
   public function getIdentifier()
   {
       return $this->identifier;
   }

   /**
    * Set displayName
    *
    * @param string $displayName
    * @return LinkedInUser
    */
   public function setDisplayName($displayName)
   {
       $this->displayName = $displayName;

       return $this;
   }

   /**
    * Get displayName
    *
    * @return string
    */
   public function getDisplayName()
   {
       return $this->displayName;
   }

   /**
    * Set firstName
    *
    * @param string $firstName
    * @return LinkedInUser
    */
   public function setFirstName($firstName)
   {
       $this->firstName = $firstName;

       return $this;
   }

   /**
    * Get firstName
    *
    * @return string
    */
   public function getFirstName()
   {
       return $this->firstName;
   }

   /**
    * Set lastName
    *
    * @param string $lastName
    * @return LinkedInUser
    */
   public function setLastName($lastName)
   {
       $this->lastName = $lastName;

       return $this;
   }

   /**
    * Get lastName
    *
    * @return string
    */
   public function getLastName()
   {
       return $this->lastName;
   }

   /**
    * Set email
    *
    * @param string $email
    * @return LinkedInUser
    */
   public function setEmail($email)
   {
       $this->email = $email;

       return $this;
   }

   /**
    * Get email
    *
    * @return string
    */
   public function getEmail()
   {
       return $this->email;
   }

   /**
    * Set profileURL
    *
    * @param string $profileURL
    * @return LinkedInUser
    */
   public function setProfileURL($profileURL)
   {
       $this->profileURL = $profileURL;

       return $this;
   }

   /**
    * Get profileURL
    *
    * @return string
    */
   public function getProfileURL()
   {
       return $this->profileURL;
   }

   /**
    * Set profileImageURL
    *
    * @param string $profileImageURL
    * @return LinkedInUser
    */
   public function setProfileImageURL($profileImageURL)
   {
       $this->profileImageURL = $profileImageURL;

       return $this;
   }

   /**
    * Get profileImageURL
    *
    * @return string
    */
   public function getProfileImageURL()
   {
       return $this->profileImageURL;
   }

   /**
    * Set location
    *
    * @param \CampaignChain\CoreBundle\Entity\Location $location
    * @return LinkedInUser
    */
   public function setLocation(\CampaignChain\CoreBundle\Entity\Location
     $location = null)
   {
       $this->location = $location;

       return $this;
   }

   /**
    * Get location
    *
    * @return \CampaignChain\CoreBundle\Entity\Location
    */
   public function getLocation()
   {
       return $this->location;
   }


   /**
    * Constructor
    */
   public function __construct()
   {

   }

}
5. Add a Channel Icon

Every Channel module should include a channel icon image, for easy identification within the CampaignChain GUI. In most cases, the channel you’re trying to connect to will provide a logo image, so all that’s really needed is to resize it to 16x16 pixels and save it in PNG format.

Note

Remember to read the channel’s terms of use for its images, ensure that your usage of the image is compliant and provide an image credit, link and/or attribution as needed.

For the LinkedIn Channel module created in this tutorial, the channel icon image should be saved to your-project/src/Acme/CampaignChain/Location/LinkedInBundle/Resources/public/images/icons/16x16/linkedin.png. The name of the image (‘linkedin’) should match the descriptive string used in the bundle name (‘acme/channel-linkedin’)

At this point, your Channel and Location bundles are complete.

Define Activities and Operations

With the Channel and Location defined, the next step is to define the Activities and Operations possible. To keep things simple, we’ll assume that only a single Activity is required: sharing news on the user’s LinkedIn stream. This will be accomplished using LinkedIn’s Share API, which makes it possible to add posts to a user’s LinkedIn social stream using REST.

1. Generate Activity and Operation bundles

The first step here is again to create bundles for the Activity and Operation. Remember that every Activity must have at least one Operation. In this case, since only one Operation is needed, the Activity is equal to the Operation (and you’ll see further along how to let CampaignChain know this).

The command to create a new bundle is explained earlier. Here it is again:

$ php app/console generate:bundle

When creating the Activity bundle, you will specify the bundle namespace as Acme/CampaignChain/Activity/LinkedInBundle. The new bundle will be created at your-project/src/Acme/CampaignChain/Activity/LinkedInBundle with the name AcmeCampaignChainActivityLinkedInBundle.

When creating the Operation bundle, you will specify the bundle namespace as Acme/CampaignChain/Operation/LinkedInBundle. The new bundle will be created at your-project/src/Acme/CampaignChain/Operation/LinkedInBundle with the name AcmeCampaignChainOperationLinkedInBundle.

2. Create Configuration Files

The next step is to create configuration files for your Activity and Operation bundles.

To begin, create the composer.json file for the Activity bundle:

// src/Acme/CampaignChain/Activity/LinkedInBundle/composer.json
{
    "name": "acme/activity-linkedin",
    "description": "Post a news update on a LinkedIn stream.",
    "keywords": ["linkedin","news"],
    "type": "campaignchain-activity",
    "homepage": "http://example.ac.me",
    "license": "Proprietary",
    "authors": [
        {
            "name": "John Doe",
            "email": "john@example.ac.me"
        }
    ],
    "require": {
        "campaignchain/core": "dev-master",
        "campaignchain/location-linkedin": "dev-master",
        "campaignchain/operation-linkedin": "dev-master",
        "campaignchain/hook-due": "dev-master"
    }
}

You will notice that the type parameter specifies the bundle type as ‘campaignchain-activity’. Notice also that the require parameter includes dependencies for the corresponding Operation bundle, as well as CampaignChain’s due hook. The latter is needed so that activities and operations can be scheduled for automatic execution at specific times in the future.

You should also create a composer.json for the Operation bundle, as shown below:

// src/Acme/CampaignChain/Operation/LinkedInBundle/composer.json
{
    "name": "acme/operation-linkedin",
    "description": "Collection of various LinkedIn operations.",
    "keywords": ["linkedin","operation"],
    "type": "campaignchain-operation",
    "homepage": "http://example.ac.me",
    "license": "Proprietary",
    "authors": [
        {
            "name": "John Doe",
            "email": "john@example.ac.me"
        }
    ]
}

By now, it should be clear that the type parameter for Operation bundles must hold the value ‘campaignchain-operation’...and that’s clearly seen in the above definition as well.

Your next step is to define the Activity bundle’s campaignchain.yml file, which specifies the display name for the LinkedIn Activity module, any routes used by the module and any hooks. Here’s what the file looks like:

# src/Acme/CampaignChain/Activity/LinkedInBundle/campaignchain.yml

modules:
  acme-linkedin-share-news-item:
    display_name: 'Share News'
    channels:
        - acme/channel-linkedin/acme-linkedin
    routes:
        new: acme_campaignchain_activity_linkedin_share_news_item_new
        edit: acme_campaignchain_activity_linkedin_share_news_item_edit
        edit_modal: acme_campaignchain_activity_linkedin_share_news_item_edit_modal
        edit_api: acme_campaignchain_activity_linkedin_share_news_item_edit_api
    hooks:
        campaignchain-due: true

Following CampaignChain conventions, the Activity module name includes the vendor name and a string that describes the purpose of the module - in this case, ‘acme-linkedin-share-news-item’. The module configuration also specifies the display name to be used in the CampaignChain GUI, the names of the Symfony route to be used when creating or editing activities, and any hooks used by the module.

An important addition in the Activity bundle’s campaignchain.yml file is the channels parameter, which specifies the link between the channel and the activity. The format of the value is the Channel bundle name, followed by the Channel module name, separated by slashes. In this case, the value ‘acme/channel-linkedin/acme-linkedin’ points to the Channel bundle created earlier (‘acme/channel-linkedin’) and the Channel module within it (‘acme-linkedin’).

Once the Activity module is defined, the next step is to tell CampaignChain about the operations supported by the activity. This is specified via the Operation bundle’s campaignchain.yml file, as shown below:

# src/Acme/CampaignChain/Operation/LinkedInBundle/campaignchain.yml

modules:
  acme-linkedin-share-news-item:
    display_name: 'Share News'
    owns_location: true
    services:
        job: acme.operation.linkedin.job.share_news_item

This configuration informs CampaignChain about the Operation module representing the news sharing operation for LinkedIn. As before, the Operation module name contains the vendor name and a brief descriptive string identifying the operation.

Since the Operation module will also create a new Location (in this case, a new post in the LinkedIn stream which is accessible directly via a unique URL), it’s important to tell CampaignChain that the Operation will own the new Location, via the owns_location parameter.

Finally, since the Operation needs to expose a Job (which will be run by CampaignChain’s global scheduler and which we’ll define further along), the configuration specifies the name for this job service so CampaignChain can easily invoke it.

3. Understand the LinkedIn Share API

Now that the basics of the bundles are defined, let’s look more closely at the news sharing operation to be implemented. Review the image below, which displays a typical news item in a LinkedIn user’s stream.

_images/linkedin-news-item.png

As you can see, a LinkedIn news item has a number of elements:

  • A user message
  • A link to an external page
  • A link title
  • A link description
  • A link image

The most efficient way to post such a news item to a LinkedIn user’s stream programmatically is with the LinkedIn Share API. Using this API involves sending an authenticated POST request to the API endpoint https://api.linkedin.com/v1/people/~/shares, and transmitting an XML document like the one shown below in the body of the POST request:

<share>
  <comment>The White House is awesome!</comment>
  <content>
    <title>The White House - President Barack Obama</title>
    <description>Opening the Doors to the White House</description>
    <submitted-url>http://whitehouse.gov</submitted-url>
    <submitted-image-url>
      https://media.licdn.com/media-proxy/ext?w=180&h=110&f=c&hash=6VU6...
    </submitted-image-url>
  </content>
  <visibility>
    <code>anyone</code>
  </visibility>
</share>

It should be easy to understand the correspondence between the XML elements and the news item components.

The response to a successful request is returned in XML and looks something like this:

<?xml version='1.0'?>
<update>
  <update-key>KEY-1111-2222-33333-KEY</update-key>
  <update-url>https://www.linkedin.com/updates?scope=111111111...</update-url>
</update>

To implement the news sharing operating in CampaignChain, therefore, you’ll first create a NewsItem entity representing a LinkedIn news item, and a service manager to work with that entity.

You’ll also need an input form, so the user can populate the entity, and a client to take care of the nitty-gritty of generating and transmitting a correctly-formatted POST request to the LinkedIn API.

Finally, because one of CampaignChain’s core capabilities is the ability to schedule activities and operations ahead of time, you’ll need to store newly-created NewsItem entities in the CampaignChain database, and implement a job to transmit them to LinkedIn at the appropriate time.

4. Create An Entity and Entity Manager

In this step, you will create an entity to represent a LinkedIn news item. The code for this NewsItem entity is as follows and it should be saved to your-project/src/Acme/CampaignChain/Operation/LinkedInBundle/Entity/NewsItem.php.

<?php

// src/Acme/CampaignChain/Operation/LinkedInBundle/Entity/NewsItem.php

namespace Acme\CampaignChain\Operation\LinkedInBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="acme_operation_linkedin_news_item")
 */
class NewsItem
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\OneToOne(targetEntity="CampaignChain\CoreBundle\Entity\Operation",
     *   cascade={"persist"})
     */
    protected $operation;

    /**
     * @ORM\Column(type="text")
     */
    protected $message;

    /**
     * @ORM\Column(type="text")
     */
    protected $linkTitle;

    /**
     * @ORM\Column(type="text")
     */
    protected $linkDescription;

    /**
     * URL included within the share content
     * @ORM\Column(type="string", length=255, name="linkUrl", nullable=true)
     */
    protected $linkUrl;

    /**
     * direct URL to the share
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    protected $url;


    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set message
     *
     * @param string $message
     * @return Status
     */
    public function setMessage($message)
    {
        $this->message = $message;

        return $this;
    }

    /**
     * Get message
     *
     * @return string
     */
    public function getMessage()
    {
        return $this->message;
    }

    /**
     * Set title
     *
     * @param string $linkTitle
     * @return Status
     */
    public function setLinkTitle($linkTitle)
    {
        $this->linkTitle = $linkTitle;

        return $this;
    }

    /**
     * Get title
     *
     * @return string
     */
    public function getLinkTitle()
    {
        return $this->linkTitle;
    }

    /**
     * Set description
     *
     * @param string $linkDescription
     * @return Status
     */
    public function setLinkDescription($linkDescription)
    {
        $this->linkDescription = $linkDescription;

        return $this;
    }

    /**
     * Get description
     *
     * @return string
     */
    public function getLinkDescription()
    {
        return $this->linkDescription;
    }

    /**
     * Set submit URL
     *
     * @param string $linkUrl
     * @return Status
     */
    public function setLinkUrl($linkUrl)
    {
        $this->linkUrl = $linkUrl;

        return $this;
    }

    /**
     * Get submit URL
     *
     * @return string
     */
    public function getLinkUrl()
    {
        return $this->linkUrl;
    }

    /**
     * Set url
     *
     * @param string $url
     * @return Status
     */
    public function setUrl($url)
    {
        $this->url = $url;

        return $this;
    }

    /**
     * Get url
     *
     * @return string
     */
    public function getUrl()
    {
        return $this->url;
    }

    /**
     * Set operation
     *
     * @param \CampaignChain\CoreBundle\Entity\Operation $operation
     * @return Status
     */
    public function setOperation(\CampaignChain\CoreBundle\Entity\Operation
      $operation = null)
    {
        $this->operation = $operation;

        return $this;
    }

    /**
     * Get operation
     *
     * @return \CampaignChain\CoreBundle\Entity\Operation
     */
    public function getOperation()
    {
        return $this->operation;
    }
}

As you can see, the entity includes proeprties corresponding to those expected by the LinkedIn Share API (the image URL field is omitted for simplicity), as well as some properties needed by CampaignChain.

You will also need an entity service manager, which will retrieve an instance of the entity by its identifier. Here’s the code, which should be saved to your-project/src/Acme/CampaignChain/Operation/LinkedInBundle/EntityService/NewsItem.php.

<?php

// src/Acme/CampaignChain/Operation/LinkedInBundle/EntityService/NewsItem.php

namespace Acme\CampaignChain\Operation\LinkedInBundle\EntityService;

use Doctrine\ORM\EntityManager;

class NewsItem
{
    protected $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function getNewsItemByOperation($id){
        $newsitem = $this->em
            ->getRepository('AcmeCampaignChainOperationLinkedInBundle:NewsItem')
            ->findOneByOperation($id);

        if (!$newsitem) {
            throw new \Exception(
                'No news item found by operation id '.$id
            );
        }

        return $newsitem;
    }
}

The getNewsItemByOperation() method takes care of retrieving a specific news item using its unique identifier in the database.

This is also a good point to update the Operation module’s list of exposed services to include the new entity service manager. To do this, update the file at your-project/src/Acme/CampaignChain/Operation/LinkedInBundle/Resources/config/services.yml with the following information.

# src/Acme/CampaignChain/Operation/LinkedInBundle/Resources/config/services.yml

parameters:

services:
    acme.operation.linkedin.news_item:
        class: Acme\CampaignChain\Operation\LinkedInBundle\EntityService\NewsItem
        arguments: [ @doctrine.orm.entity_manager ]
5. Create an Input Form for Entity Data

With the entity created, the next step is to provide an input form that will be rendered by the CampaignChain user interface. This form will be used when setting up a new LinkedIn news item, and the fields in the form must therefore correspond with the properties of the NewsItem entity.

The easiest way to create the form is by using Symfony’s Form component and FormBuilder interface. The following code, which should be saved to your-project/src/Acme/CampaignChain/Operation/LinkedInBundle/Form/Type/ShareNewsItemOperationType.php, illustrates how to do this.

<?php

// src/Acme/CampaignChain/Operation/LinkedInBundle/Form/Type/ShareNewsItemOperationType.php

namespace Acme\CampaignChain\Operation\LinkedInBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\ORM\EntityManager;

class ShareNewsItemOperationType extends AbstractType
{
    private $newsitem;
    private $view = 'default';
    protected $em;
    protected $container;

    public function __construct(EntityManager $em, ContainerInterface $container)
    {
        $this->em = $em;
        $this->container = $container;
    }

    public function setNewsItem($newsitem){
        $this->newsitem = $newsitem;
    }

    public function setView($view){
        $this->view = $view;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('message', 'text', array(
                'property_path' => 'message',
                'label' => 'Message',
                'attr' => array(
                    'placeholder' => 'Add message...',
                    'max_length' => 200
                )
            ));
        $builder
            ->add('linkTitle', 'text', array(
                'property_path' => 'linkTitle',
                'label' => 'Title of page being shared',
                'attr' => array(
                    'placeholder' => 'Add title...',
                    'max_length' => 140
                )
            ));
        $builder
            ->add('description', 'textarea', array(
                'property_path' => 'linkDescription',
                'label' => 'Description of page being shared',
                'attr' => array(
                    'placeholder' => 'Add description...',
                    'max_length' => 300
                )
            ));
        $builder
            ->add('submitUrl', 'text', array(
                'property_path' => 'linkUrl',
                'label' => 'URL of page being shared',
                'attr' => array(
                    'placeholder' => 'Add URL...',
                    'max_length' => 255
                )
            ));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $defaults = array(
            'data_class' =>
              'CampaignChain\Operation\LinkedInBundle\Entity\NewsItem',
        );

        if($this->newsitem){
            $defaults['data'] = $this->newsitem;
        }
        $resolver->setDefaults($defaults);
    }

    public function getName()
    {
        return 'acme_operation_linkedin_share_news_item';
    }
}

The main work here is done by the buildForm() method, which takes care of creating the necessary form fields, and the setDefaultOptions() method, which links the data entered into the form with the NewsItem entity created earlier.

6. Create an API Client and Job Processor

In the previous steps, you enabled the user to enter details of a new LinkedIn news item into a form and have that data saved to the CampaignChain database. The next step is to massage that data into the format needed by the LinkedIn API and then transfer it to the API in an authenticated request.

To accomplish this task, it is necessary to create an HTTP client object which will ease communication with the LinkedIn API. CampaignChain already comes with an OAuth client, which you used previously in your LinkedIn Channel module. You can use this client’s built-in functionality to take care of most of the authentication tasks.

To do this, go back to your Channel module and add the following LinkedInClient object to it, at the location your-project/src/Acme/CampaignChain/Channel/LinkedInBundle/REST/LinkedInClient.php.

<?php

// src/Acme/CampaignChain/Channel/LinkedInBundle/REST/LinkedInClient.php

namespace Acme\CampaignChain\Channel\LinkedInBundle\REST;

use Symfony\Component\HttpFoundation\Session\Session;
use Guzzle\Http\Client;
use Guzzle\Plugin\Oauth\OauthPlugin;

class LinkedInClient
{
    const RESOURCE_OWNER = 'LinkedIn';
    const BASE_URL   = 'https://api.linkedin.com/v1';

    protected $container;

    public function setContainer($container)
    {
        $this->container = $container;
    }

    public function connectByActivity($activity){
        $oauthApp = $this->container->get(
          'campaignchain.security.authentication.client.oauth.application'
        );
        $application = $oauthApp->getApplication(self::RESOURCE_OWNER);

        $oauthToken = $this->container->get(
          'campaignchain.security.authentication.client.oauth.token'
        );
        $token = $oauthToken->getToken($activity->getLocation());

        return $this->connect(
          $application->getKey(),
          $application->getSecret(),
          $token->getAccessToken(),
          $token->getTokenSecret()
        );
    }

    public function connect($appKey, $appSecret, $accessToken, $tokenSecret) {
        try {
            $client = new Client(self::BASE_URL.'/');
            $oauth  = new OauthPlugin(array(
                'consumer_key'    => $appKey,
                'consumer_secret' => $appSecret,
                'token'           => $accessToken,
                'token_secret'    => $tokenSecret,
            ));

            return $client->addSubscriber($oauth);
        }
        catch (ClientErrorResponseException $e) {
            $request = $e->getRequest();
            $response = $e->getResponse();
            print_r($response);
        }
        catch (ServerErrorResponseException $e) {
            $request = $e->getRequest();
            $response = $e->getResponse();
            print_r($response);
        }
        catch (BadResponseException $e) {
            $request = $e->getRequest();
            $response = $e->getResponse();
            print_r($response);
        }
        catch(Exception $e){
          print_r($e->getMessage());
        }
    }
}

The two important values set in this client are the constants at the top: the RESOURCE_OWNER constant specifies the owning channel, which is then used to retrieve the keys and secrets needed for an authenticated API connection, and the BASE_URL constant specifies the base URL for all API requests.

You will also need to update the Channel module’s list of exposed services to include the new client. To do this, update the file at your-project/src/Acme/CampaignChain/Channel/LinkedInBundle/Resources/config/services.yml with the following information.

# src/Acme/CampaignChain/Channel/LinkedInBundle/Resources/config/services.yml

parameters:

services:
    acme.channel.linkedin.rest.client:
        class: Acme\CampaignChain\Channel\LinkedInBundle\REST\LinkedInClient
        calls:
            - [setContainer, ["@service_container"]]

You’ll notice that this client object merely takes care of connecting and authenticating against the LinkedIn API. It doesn’t actually take care of creating and sending a POST request to the Share API. That task is handled by a separate Job object, which should be created within your Operation module at your-project/src/Acme/CampaignChain/Operation/LinkedInBundle/Job/ShareNewsItem.php.

<?php

// src/Acme/CampaignChain/Operation/LinkedInBundle/Job/ShareNewsItem.php

namespace Acme\CampaignChain\Operation\LinkedInBundle\Job;

use CampaignChain\CoreBundle\Entity\Action;
use Doctrine\ORM\EntityManager;
use CampaignChain\CoreBundle\Entity\Medium;
use CampaignChain\CoreBundle\Job\JobServiceInterface;
use Symfony\Component\HttpFoundation\Response;

class ShareNewsItem implements JobServiceInterface
{
    protected $em;
    protected $container;

    protected $message;
    protected $linkTitle;
    protected $linkDescription;
    protected $linkUrl;

    public function __construct(EntityManager $em, $container)
    {
        $this->em = $em;
        $this->container = $container;
    }

    public function execute($operationId)
    {
        $newsitem = $this->em
          ->getRepository('AcmeCampaignChainOperationLinkedInBundle:NewsItem')
          ->findOneByOperation($operationId);

        if (!$newsitem) {
            throw new \Exception(
              'No news item found for an operation with ID: '.$operationId
            );
        }

        // Process the link URL to append the Tracking ID attached for
        // call to action tracking.
        $ctaService = $this->container->get('campaignchain.core.cta');
        $newsitem->setLinkUrl(
            $ctaService->processCTAs(
                $newsitem->getLinkUrl(),
                $newsitem->getOperation()
            )
            ->getContent()
        );

        $client = $this->container->get('acme.channel.linkedin.rest.client');
        $connection = $client->connectByActivity(
          $newsitem->getOperation()->getActivity()
        );

        $xmlBody = "<share><comment>" . $newsitem->getMessage() .
          "</comment><content><title>" . $newsitem->getLinkTitle() .
          "</title><description>" . $newsitem->getLinkDescription() .
          "</description><submitted-url>" . $newsitem->getLinkUrl() .
          "</submitted-url></content><visibility><code>anyone</code></visibility></share>";

        $request = $connection->post(
          'people/~/shares',
          array('headers' => array('Content-Type' => 'application/xml')),
          $xmlBody
        );
        $response = $request->send()->xml();

        $newsitemUrl = (string)$response->{'update-url'};
        $newsitemId = (string)$response->{'update-key'};

        $newsitem->setUrl($newsitemUrl);
        // Set Operation to closed.
        $newsitem->getOperation()->setStatus(Action::STATUS_CLOSED);

        $location = $newsitem->getOperation()->getLocations()[0];
        $location->setIdentifier($newsitemId);
        $location->setURL($newsitemUrl);
        $location->setName($status->getOperation()->getName());
        $location->setStatus(Medium::STATUS_ACTIVE);

        $this->em->flush();

        $this->message = 'The message "'.$newsitem->getMessage().'" with the ID "'.
          $newsitemId.'" has been posted on LinkedIn. See it on LinkedIn:
          <a href="'.$newsitemUrl.'">'.$newsitemUrl.'</a>';

        return self::STATUS_OK;

    }

    public function getMessage(){
        return $this->message;
    }
}

A Job object is always part of an Operation module and it is called as necessary to perform the corresponding operation. It should implement the JobServiceInterface, which mandates an execute() method which is called when the job is executed.

If you look into the execute() method above, you’ll see that it begins by retrieving the required news item from the CampaignChain database (using the news item’s identifier). Then it passes the link URL to CampaignChain’s CTA service so that CampaignChain can store the URL as a Call to Action and track it. It also invokes the LinkedIn client created earlier as a Symfony service and uses the client to authenticate against the LinkedIn API.

The next step is to generate an XML document containing the details of the news item to be posted, in the format expected by the LinkedIn API. This XML document is then transmitted to the API endpoint https://api.linkedin.com/v1/people/~/shares in a POST request using the client’s inherited post() method. The XML response is converted to a SimpleXML object for easy processing.

The XML response contains two useful pieces of information: the LinkedIn identifier for the news item, and the direct URL to it. The remainder of the execute() method is concerned with saving this information to the CampaignChain database, updating the status of the operation and presenting a success message to the user.

Given that the shared news has a dedicated URL on Linkedin, a new Location is being created. That way, the new posting will also be included in CampaignChain’s Call-to-Action tracking.

Finally, update the Activity module’s list of exposed services to include the new job. Remember that the name you assign to this job service must match the name specified for the job in the Activity module’s campaignchain.yml file.

To do this, update the file at your-project/src/Acme/CampaignChain/Activity/LinkedInBundle/Resources/config/services.yml so it now looks like the following.

# src/Acme/CampaignChain/Activity/LinkedInBundle/Resources/config/services.yml

parameters:

services:
    acme.operation.linkedin.job.share_news_item:
        class: Acme\CampaignChain\Operation\LinkedInBundle\Job\ShareNewsItem
        arguments: [ @doctrine.orm.entity_manager, @service_container ]
    acme.operation.linkedin.news_item:
            class: Acme\CampaignChain\Operation\LinkedInBundle\EntityService\NewsItem
            arguments: [ @doctrine.orm.entity_manager ]
7. Create Activity and Operation Routes

In general, an Activity module should specify the routes for creating and editing operations. This implies that the Activity module should define four routes:

  • A route to create a new activity (‘new’)
  • A route to edit an existing activity (‘edit’)
  • A route to edit an existing activity in the campaign timeline’s pop-up/lightbox view (‘edit_modal’)
  • A route for the submit action of the pop-up/lightbox view in the campaign timeline (‘edit_api’)

When defining the campaignchain.yml file for the Activity module, you specified names for all these routes. The next step is to connect those names with Symfony controllers and actions.

To do this, update the file your-project/src/Acme/CampaignChain/Activity/LinkedInBundle/Resources/config/routing.yml as shown below:

# src/Acme/CampaignChain/Activity/LinkedInBundle/Resources/config/routing.yml

acme_campaignchain_activity_linkedin_share_news_item_new:
  pattern:  /activity/linkedin/share-news-item/new
  defaults: { _controller: AcmeCampaignChainActivityLinkedInBundle:ShareNewsItem:new }

acme_campaignchain_activity_linkedin_share_news_item_edit:
  pattern:  /activity/linkedin/share-news-item/{id}/edit
  defaults: { _controller: AcmeCampaignChainActivityLinkedInBundle:ShareNewsItem:edit }

acme_campaignchain_activity_linkedin_share_news_item_edit_modal:
  pattern:  /modal/activity/linkedin/share-news-item/{id}/edit
  defaults: { _controller: AcmeCampaignChainActivityLinkedInBundle:ShareNewsItem:editModal }

acme_campaignchain_activity_linkedin_share_news_item_edit_api:
  pattern:  /api/private/activity/linkedin/share-news-item/byactivity/{id}/edit
  defaults: { _controller: AcmeCampaignChainActivityLinkedInBundle:ShareNewsItem:editApi }
  options:
    expose: true

Note

You can delete the default ‘hello’ route added by the Symfony bundle generator in the above file. Similarly, you can delete the default ‘hello’ route in the Operation module’s routing.xml file, which can be found at your-project/src/Acme/CampaignChain/Operation/LinkedInBundle/Resources/config/routing.yml.

8. Create an Activity Controller

Next, you’ll need to create views and controllers for the routes above. First up, you’ll handle the ‘new’ route, by creating a controller with a createAction() method, as shown below.

<?php

// src/Acme/CampaignChain/Activity/LinkedInBundle/Controller/ShareNewsItemController.php

namespace Acme\CampaignChain\Activity\LinkedInBundle\Controller;

use CampaignChain\CoreBundle\Entity\Location;
use CampaignChain\CoreBundle\Entity\Medium;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Session\Session;
use CampaignChain\CoreBundle\Entity\Operation;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Acme\CampaignChain\Operation\LinkedInBundle\Form\Type\ShareNewsItemOperationType;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;

class ShareNewsItemController extends Controller
{
    const BUNDLE_NAME = 'acme/activity-linkedin';
    const MODULE_IDENTIFIER = 'acme-linkedin-share-news-item';
    const OPERATION_IDENTIFIER = self::MODULE_IDENTIFIER;

    public function newAction(Request $request)
    {
        $wizard = $this->get('campaignchain.core.activity.wizard');
        $campaign = $wizard->getCampaign();
        $activity = $wizard->getActivity();

        $activity->setEqualsOperation(true);

        $activityType = $this->get('campaignchain.core.form.type.activity');
        $activityType->setBundleName(self::BUNDLE_NAME);
        $activityType->setModuleIdentifier(self::MODULE_IDENTIFIER);
        $shareNewsItemOperation = new ShareNewsItemOperationType(
          $this->getDoctrine()->getManager(), $this->get('service_container'));
        $operationForms[] = array(
            'identifier' => self::OPERATION_IDENTIFIER,
            'form' => $shareNewsItemOperation,
            'label' => 'LinkedIn Message',
        );
        $activityType->setOperationForms($operationForms);
        $activityType->setCampaign($campaign);

        $form = $this->createForm($activityType, $activity);

        $form->handleRequest($request);

        if ($form->isValid()) {
            $activity = $wizard->end();

            // Get the operation module.
            $operationService = $this->get('campaignchain.core.operation');
            $operationModule = $operationService->getOperationModule(
                'acme/operation-linkedin',
                'acme-linkedin-share-news-item'
            );

            // The activity equals the operation.
            // Thus, we create a new operation with the same data.
            $operation = new Operation();
            $operation->setName($activity->getName());
            $operation->setActivity($activity);
            $activity->addOperation($operation);
            $operationModule->addOperation($operation);
            $operation->setOperationModule($operationModule);

            // The Operation creates a Location, i.e. the post
            // will be accessible through a URL after publishing.
            // Get the location module for the user stream.
            $locationService = $this->get('campaignchain.core.location');
            $locationModule = $locationService->getLocationModule(
                'acme/location-linkedin',
                'acme-linkedin-user'
            );

            $location = new Location();
            $location->setLocationModule($locationModule);
            $location->setParent($activity->getLocation());
            $location->setName($activity->getName());
            $location->setStatus(Medium::STATUS_UNPUBLISHED);
            $location->setOperation($operation);
            $operation->addLocation($location);

            // Get the status data from request.
            $status = $form->get(self::OPERATION_IDENTIFIER)->getData();
            // Link the status with the operation.
            $status->setOperation($operation);

            $repository = $this->getDoctrine()->getManager();

            // Make sure that data stays intact by using transactions.
            try {
                $repository->getConnection()->beginTransaction();

                $repository->persist($activity);
                $repository->persist($status);

                // We need the activity ID for storing the hooks. Hence we must flush here.
                $repository->flush();

                $hookService = $this->get('campaignchain.core.hook');
                $activity = $hookService->processHooks(self::BUNDLE_NAME,
                  self::MODULE_IDENTIFIER, $activity, $form, true);

                $repository->flush();

                $repository->getConnection()->commit();
            } catch (\Exception $e) {
                $repository->getConnection()->rollback();
                throw $e;
            }

            $this->get('session')->getFlashBag()->add(
                'success',
                'Your new LinkedIn activity <a href="'.
                $this->generateUrl(
                  'campaignchain_core_activity_edit',
                  array('id' => $activity->getId())
                ).
                '">'.$activity->getName().
                '</a> was created successfully.'
            );

            if ($form->get('campaignchain_hook_campaignchain_due')->has('execution_choice')
              && $form->get('campaignchain_hook_campaignchain_due')
                      ->get('execution_choice')->getData() == 'now') {
                $job = $this->get('acme.operation.linkedin.job.share_news_item');
                $job->execute($operation->getId());
            }

            return $this->redirect(
              $this->generateUrl('campaignchain_core_activities')
            );

        }

        return $this->render(
            'CampaignChainCoreBundle:Base:new.html.twig',
            array(
                'page_title' => 'New LinkedIn News Item',
                'page_secondary_title' => 'Campaign "'.$campaign->getName().'"',
                'form' => $form->createView(),
            ));

    }
}

The createAction() method begins by initializing CampaignChain’s Activity Wizard, which takes care of presenting the user with a form that lists available campaigns and activities. Based on the information received in the form, the Activity Wizard gets references to the correct Campaign and Activity. Since by design this Activity has only one Operation, the setEqualsOperation() method is used to tell CampaignChain that the Activity and the Operation are to be treated as the same entity.

The controller then initializes and renders the Form object created earlier using the createForm() method, so that the user can enter the necessary details for the Activity - in this case, the news item to be posted. If the form input is valid, the script creates a new Operation object with the same data as the Activity object. The Operation is then added to the Activity with the Activity object’s addOperation() method.

At the same time, once the Operation succeeds, a new Location will be created representing the news item on LinkedIn. Therefore, the controller invokes the Location service and defines basic data for the Location. This Location record is by necessity incomplete at this stage as the news item has yet to be published on LinkedIn; if you refer to the Job created earlier, you will see that the Job updates the Location record with the URL to the news item once it is executed.

Once all the relationships are established, the data is saved to the CampaignChain database and a success message is displayed to the user. The final execution of the operation is handled by the CampaignChain job scheduler at the appropriate time. The above code however demonstrates how the operation can be executed immediately if required, by invoking the Job service and calling the Job’s execute() method.

In a similar vein, you can handle the ‘edit’ route by defining an editAction() method, which takes care of editing an existing activity/operation.

<?php

// src/Acme/CampaignChain/Activity/LinkedInBundle/Controller/ShareNewsItemController.php

namespace CampaignChain\Activity\LinkedInBundle\Controller;

use CampaignChain\CoreBundle\Entity\Location;
use CampaignChain\CoreBundle\Entity\Medium;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Session\Session;
use CampaignChain\CoreBundle\Entity\Operation;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Acme\CampaignChain\Operation\LinkedInBundle\Form\Type\ShareNewsItemOperationType;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;

class ShareNewsItemController extends Controller
{

    public function editAction(Request $request, $id)
    {
        $activityService = $this->get('campaignchain.core.activity');
        $activity = $activityService->getActivity($id);
        $campaign = $activity->getCampaign();

        // Get the one operation.
        $operation = $activityService->getOperation($id);
        $operationService = $this->get('acme.operation.linkedin.news_item');
        $newsitem = $operationService->getNewsItemByOperation($operation);

        $activityType = $this->get('campaignchain.core.form.type.activity');
        $activityType->setBundleName(self::BUNDLE_NAME);
        $activityType->setModuleIdentifier(self::MODULE_IDENTIFIER);
        $shareNewsItemOperation = new ShareNewsItemOperationType(
          $this->getDoctrine()->getManager(), $this->get('service_container')
        );
        $shareNewsItemOperation->setNewsItem($newsitem);
        $operationForms[] = array(
            'identifier' => self::OPERATION_IDENTIFIER,
            'form' => $shareNewsItemOperation,
            'label' => 'LinkedIn Message',
        );
        $activityType->setOperationForms($operationForms);
        $activityType->setCampaign($campaign);

        $form = $this->createForm($activityType, $activity);

        $form->handleRequest($request);

        if ($form->isValid()) {
            // Get the status data from request.
            $status = $form->get(self::OPERATION_IDENTIFIER)->getData();

            $repository = $this->getDoctrine()->getManager();

            // The activity equals the operation.
            // Thus, we update the operation with the same data.
            $activityService = $this->get('campaignchain.core.activity');
            $operation = $activityService->getOperation($id);
            $operation->setName($activity->getName());
            $repository->persist($operation);

            $repository->persist($status);

            $hookService = $this->get('campaignchain.core.hook');
            $activity = $hookService->processHooks(
              self::BUNDLE_NAME, self::MODULE_IDENTIFIER, $activity, $form
            );
            $repository->persist($activity);

            $repository->flush();


            $this->get('session')->getFlashBag()->add(
                'success',
                'Your LinkedIn activity <a href="'.
                $this->generateUrl(
                  'campaignchain_core_activity_edit',
                  array('id' => $activity->getId())
                ).'">'.$activity->getName().
                '</a> was edited successfully.'
            );

            if ($form->get('campaignchain_hook_campaignchain_due')->has('execution_choice')
              && $form->get('campaignchain_hook_campaignchain_due')
                      ->get('execution_choice')->getData() == 'now') {
                $job = $this->get('acme.operation.linkedin.job.share_news_item');
                $job->execute($operation->getId());
            }

            return $this->redirect($this->generateUrl('campaignchain_core_activities'));
        }

        return $this->render(
            'CampaignChainCoreBundle:Base:new.html.twig',
            array(
                'page_title' => 'Edit LinkedIn News Item',
                'page_secondary_title' => 'Campaign "'.$campaign->getName().'"',
                'form' => $form->createView(),
            ));
    }
}

The editAction() action method is very similar to the createAction() method described previously, with the primary difference being that it uses the identifier passed in the URL string to retrieve a specific activity or operation and pre-populate the input form with the details of that activity or operation.

Note

The editModalAction() and editApiAction() method implement functionality similar to that of the editAction() method. They are not included here but can be viewed in the source code of the corresponding LinkedIn bundle on Github.

It’s important to note that all the action methods described above use CampaignChain’s base views, and it is not necessary to create new views unless you specifically wish to override the base views.

At this point, your Activity and Operation bundles are complete. Once you add your modules to CampaignChain through the module installer, you should be able to connect to a new LinkedIn location and begin posting news items to it.

Administrators

Documentation that explains how to install and configure CampaignChain.

The Administrator Handbook

Installation

Community Edition (CE)

The CampaignChain Community Edition has all basic modules included and you can easily add more of them inside the application.

0. Preparation
  1. Verify that your system meets the minimum system requirements to run CampaignChain.
  2. Ensure that your MySQL server is running.
1. Set up Database

Launch your MySQL client of choice and create a new MySQL database for the application.

2. Install Composer

CampaignChain utilizes Composer for its package and modules management. Install it with this command:

$ curl -sS https://getcomposer.org/installer | php
3. Install Bower

For JavaScript components, CampaignChain makes use of Bower, which - you guessed it - is a package manager for JavaScript code.

Before you can install Bower, you must first install npm which ships with node.js.

Now install Bower through npm:

$ npm install -g bower
4. Install Base System

In a folder of your choice, execute Composer to download all files of the CampaignChain base system. Please note that this might take a while.

$ composer create-project campaignchain/campaignchain-ce campaignchain 1.0.0-alpha.4
5. Configure Base System

During the process, Composer will ask in the command line to provide some configuration parameters. Please make sure you check/provide at least the following (default values in brackets):

database_driver (pdo_mysql):
database_host (127.0.0.1):
database_port (null):
database_name (campaignchain_ce):
database_user (root):
database_password (null):
java_path (/usr/bin/java):
6. Clear Cache and Dump Assets

Once Composer is done, execute the following commands, still inside the CampaignChain root folder:

$ php app/console cache:clear --env=prod --no-debug
$ php app/console assetic:dump --env=prod --no-debug
7. Configure CampaignChain Scheduler

The CampaignChain scheduler is a PHP script that executes scheduled Operations.

On Linux or Mac OS X, configure it as a cron job so that it runs automatically every minute:

$ crontab -e -u <username>
*/1 * * * * /usr/bin/php /path/to/campaignchain/app/console campaignchain:scheduler

On Windows, you could use the task scheduler or AT command to achieve the same.

8. Start Server

Use PHP’s built-in Web server to run CampaignChain.

$ php app/console server:run

By default, the built-in Web server listens for connections on 127.0.0.1. If you’re planning to connect to the server over a network, you can specify the network IP address that the server should use. For example, the command below runs the Web server on port 80 of IP address 192.168.1.1:

$ php app/console server:run 192.168.1.1:80
9. Installation Wizard

Hop over to http://localhost:8000/campaignchain/install.php and follow the instructions.

10. Install Modules

You can easily add modules (e.g. to post on Twitter or Facebook) at http://localhost:8000/modules/new/.

Success!

CampaignChain is now installed, configured and ready for use!

To make full use of CampaignChain’s capabilities, you could now

  1. Configure Call to Action (CTA) tracking
  2. Learn how to create your first campaign and activity
Development

If you would like to develop modules with CampaignChain, this page will explain how to install and configure all the relevant parts to get started.

0. Prerequisites

Verify that your system meets the minimum system requirements to run CampaignChain.

1. Install Composer

CampaignChain utilizes Composer for dependency management of PHP packages.

Install Composer to be able to install and update CampaignChain and related packages.

2. Install Bower

For JavaScript components, CampaignChain makes use of Bower, which - you guessed it - is a package manager for JavaScript code.

Before you can install Bower, you must first install npm which ships with node.js.

Now install Bower through npm:

$ npm install -g bower
3. Install Git

The CampaignChain source code is available on GitHub. Hence, please install Git to access the project and to commit to it.

4. Set up Database

CampaignChain has been tested to work with MySQL at the current point in time. Hence, please set up a MySQL server and create a new database for CampaignChain.

5. Clone from GitHub

Clone the CampaignChain CE source code from https://github.com/CampaignChain/campaignchain-ce

For example, if you set up GitHub with SSH, issue this command:

$ git clone git@github.com:CampaignChain/campaignchain-ce.git
6. Require Additional Modules and Packages

If you would like to develop with additional Composer packages or have some CampaignChain modules available right after installation, you can add them now to the composer.json file in the root of the repository you just cloned.

For example, you could add these CampaignChain modules:

"require": {
    ...
    "campaignchain/operation-twitter": "dev-master",
    "campaignchain/operation-facebook": "dev-master",
    "campaignchain/operation-linkedin": "dev-master",
    "campaignchain/location-website": "dev-master"
},
6. Install Base System

In the root of CampaignChain, execute composer to install all the packages required by the base system.

Note

You must not execute below command as the root user on Linux.

$ composer install --prefer-source

It will download and install all required packages and modules for the CampaignChain base system. Please note that this might take a while.

7. Configure Base System

During the process, Composer will ask in the command line to provide some configuration parameters. Please make sure you check/provide at least the following (default values in brackets):

database_driver (pdo_mysql):
database_host (127.0.0.1):
database_port (null):
database_name (campaignchain_ce):
database_user (root):
database_password (null):
java_path (/usr/bin/java):
8. Configure Scheduler

The CampaignChain scheduler is a PHP script that executes scheduled Operations.

On Linux or Mac OS X, configure it as a cron job so that it runs automatically every minute:

$ crontab -e -u <username>
*/1 * * * * /usr/bin/php /path/to/campaignchain/app/console campaignchain:scheduler

On Windows, you could use the task scheduler or AT command to achieve the same.

9. Start Server

Use PHP’s built-in Web server to run CampaignChain.

$ php app/console server:run

By default, the built-in Web server listens for connections on 127.0.0.1. If you’re planning to connect to the server over a network, you can specify the network IP address that the server should use. For example, the command below runs the Web server on port 80 of IP address 192.168.1.1:

$ php app/console server:run 192.168.1.1:80
10. Installation Wizard

Hop over to http://localhost:8000/campaignchain/install.php and follow the instructions.

11. Install Modules

You can easily add modules (e.g. to post on Twitter or Facebook) at http://localhost:8000/modules/new/.

Start Developing!

You can now start developing with CampaignChain and create your own bundles that include CampaignChain modules in the src/ directory inside the Symfony root.

If you would like to enhance or fix existing CampaignChain modules, they are located at /path/to/campaignchain/vendor/campaignchain/.

Configuration

Call to Action Configuration

To enable CTA tracking for a Channel, follow these steps:

1. Connect Channel

In the CampaignChain header menu, click the Create New button and select Location.

_images/create_new_location.png

Then, choose the appropriate Location from the drop-down list, e.g. Website to include a Website into CTA tracking.

_images/select_new_website_location.png

Fill in the required data to connect the Channel. For example, provide the base URL of a Website (you can omit adding pages of the Website).

_images/connect_new_website_channel.png

Once you’re done, CampaignChain will display a list of connected Channels to you. This list will include the unique Channel Tracking ID that has been assigned by CampaignChain to your new Channel. You will need this ID in the next step.

_images/channels_list.png
2. Include Tracking Code

First, include a JavaScript file provided by CampaignChain in the HTML of the online channel you plan to include.

The file is named campaignchain_tracking.js and once you have it included, it will take care of sending all the information for tracking CTAs to your CampaignChain instance.

Include the file by adding the code below to your channel, ideally right before the closing body element (i.e. </body> element) and make sure that it appears on all pages of the Channel.

<script type="text/javascript" src="[CAMPAIGNCHAIN INSTALLATION]/bundles/campaignchaincore/js/campaignchain/campaignchain_tracking.js"></script>
<script type="text/javascript">
    var campaignchainChannel = '[CAMPAIGNCHAIN CHANNEL TRACKING ID]';
</script>

Replace [CAMPAIGNCHAIN INSTALLATION] with the URL of the root of your CampaignChain installation, e.g. http://www.example.com/bundles/campaignchaincore/js/campaignchain/campaignchain_tracking.js.

Next, replace [CAMPAIGNCHAIN CHANNEL TRACKING ID] with the ID generated by CampaignChain for your channel.

OAuth Apps

To access the REST APIs of Channels such as Twitter or Facebook, CampaignChain must be registered as an App with these Channels to receive an App Key and App Secret. For example, you can do so at

To enable CampaignChain modules to access the respective REST APIs, the App Keys and Secrets can be configured within CampaignChain as follows.

In the CampaignChain header menu, click the Settings icon and select OAuth Apps.

_images/settings_menu_oauth_apps.png

In the list of OAuth Apps, pick the entry you’d like to edit by clicking on the Edit icon.

_images/oauth_apps_list.png

Change the Key and Secret field and click Save.

_images/oauth_app_edit.png

System Requirements

To run CampaignChain, your system must meet the following requirements:

  • Composer (https://getcomposer.org/)
  • PHP 5.4 or better
  • PHP’s JSON, PDO and intl extensions enabled
  • PHP’s system() function must work
  • MySQL 5.5 or better
  • Java 1.5 or better

Users

Tips & tricks for marketers to make the best use of CampaignChain.

The User Manual

What is CampaignChain?

CampaignChain is open-source campaign management software to plan, execute and monitor digital marketing campaigns across multiple online communication channels, such as Twitter, Facebook, Google Analytics or third-party CMS, e-commerce and CRM tools.

For marketers, CampaignChain enables marketing managers to have a complete overview of digital campaigns and provides one entry point to multiple communication channels for those who implement campaigns.

Key Features

CampaignChain covers three main areas of outbound and inbound campaign management:

Planning

  • Define campaign goals and milestones.
  • Create and schedule campaign activities and operations on multiple online channels.
  • View and modify campaign activities and operations using an interactive timeline.

Execution

  • Automatically execute scheduled activities and operations.
  • Collect data for monitoring during campaign duration.
  • Automatically notify responsible persons if errors occur during campaign execution.

Monitoring

  • Analytics reports: Channel-specific reporting and analytics (number of Facebook views and comments, number of Twitter retweets, and so on) for accurate campaign ROI measurement.
  • Budget reports: Defining budgets and spend per channel.
  • Sales reports: Integrate with CRM and other tools to view and analyze leads generated by each campaign.
Basic Concepts

CampaignChain’s software architecture has been designed along digital marketing terms and concepts in a specialized way, so this section gets you up to speed on CampaignChain’s terminology and explains the main entities to you.

CampaignChain knows two types of entities, a Medium and an Action, which are:

Medium Action
  • Channel
  • Location
  • Campaign
  • Milestone
  • Activity
  • Operation
Campaigns

Campaigns are at the core of CampaignChain, and are the “DNA of modern digital marketing”[1]. In CampaignChain, every campaign uses one or more communication channels. Campaigns also have milestones and activities.

Campaigns usually come in two variants: manually scheduled campaigns, which have a defined start and end date, and triggered campaigns (also called nurtured campaigns), which occur in response to user events. A campaign focused on a new product launch is an example of the former, whereas a drip email campaign that begins when a user fills up a registration form is an example of the latter.

Channels & Locations

Campaigns use online channels, which are the pathways by which campaign content reaches its audience. Common examples of channels include websites, blogs and social networks like Facebook and LinkedIn. For monitoring purposes, CampaignChain also allows connections to channels to retrieve traffic statistics (e.g. Google or YouTube Analytics) and lead generation data maintained in a CRM.

Every channel includes one or more locations, which allow granular publishing of campaign content. For example, a Twitter channel has only one location: the Twitter stream. However, a website channel might have various locations: a landing page, a banner on the home page, a “Contact Us” page with a form, and so on. Similarly, a LinkedIn channel might consist of two locations: a company profile page and a news stream. Locations are being created when connecting to a new Channel.

Furthermore, Locations can be created by an Operation. For example, an Operation that posts a Tweet on a Twitter stream is essentially creating a new Location (i.e. that Tweet) within a Location (i.e. a Twitter user’s stream). Learn more about Operations below.

Milestones

Milestones are key events or reference points during a campaign. For example, the campaign go-live date could be a milestone, and a press tour could be a second milestone. When you set up campaign milestones, related actions can be defined. For example, you could compare analytics data between two milestones. Or you could notify a member of your marketing team to start working on the next set of tasks once a milestone has been reached.

Activities and Operations

Every location allows one or more activities which can be undertaken. For example, creating a new post is an example of an activity for a blog channel.

Every activity must always have at least one operation. For example, posting on Twitter is one activity which equals the operation.

In other cases, a single activity may encompass multiple operations. For example, defining and creating a Google AdWords campaign that runs for 3 months is a possible activity for the Google AdWords channel. However, this activity could consist of two operations: the first operation might be a Google Ad that runs for the first 2 months of the campaign, and the second operation would be a second, different Google Ad that runs for the remaining 4 weeks.

User Interface

CampaignChain’s Web-based user interface is responsive and works on Desktop computers as well as mobile devices such as Tablets and Smartphones.

Footnotes
[1]This terminology was used by Lars Trieloff in his Feb 2014 presentation, which also inspires CampaignChain’s architecture.

Get Started

This is a brief step-by-step guide on how to create your first marketing Campaign and Activity within CampaignChain. You will learn how to connect Twitter as a Channel and how to post a Tweet on the stream of the related Twitter user account.

1. Connect to a Channel

To connect a Twitter Channel with CampaignChain, click the Create New button in the header menu and choose Location.

_images/create_new_location.png

On the next screen, select Twitter as the Channel and click Next.

_images/select_new_twitter_location.png

Note

Should you now see the Provide Application Credentials screen, then please ask the CampaignChain administrator to do this for you and proceed as follows.

When clicking the button Connect with Twitter, the login screen for Twitter will be displayed to you. Please enter your Twitter user name and your Twitter password.

_images/twitter_channel_login.png

If Twitter accepted your credentials, the stream of the Twitter user you logged in as will now be available as a Channel Location within CampaignChain.

2. Create a Campaign

An Activity such as posting on Twitter can only be created from within a Campaign. Click the Create New button in the header and choose Campaign.

_images/create_new_campaign.png

Select the campaign type Scheduled Campaign and proceed with Next.

_images/select_scheduled_campaign.png

Fill in the fields to populate your new Campaign with data, such as:

  • Name: An arbitrary name of your Campaign, e.g. “Launch of new product”
  • Timezone: The timezone of the Campaign. For international marketing teams, the best choice is UTC.
  • Duration: Pick the start and end date of your Campaign.
  • Assignee: The person in your team responsible for the Campaign.

Click Save and your first Campaign will be created.

_images/create_new_campaign_form.png

If you now click Plan in the header navigation, you will see your new Campaign in the Timeline.

_images/timeline.png
3. Create an Activity

Now you are ready to create your fist Activity, which will be posting a status update on Twitter.

Click the Create New button in the header and choose Activity.

_images/create_new_activity.png

In the next screen, select your newly created Campaign and in the Location field, pick the Twitter user stream you just connected to.

Once you have selected the Location, a new field will appear which allows you to select the Activity you want to perform within the Location. Here, choose Update Status and click Next.

_images/create_new_activity_form.png

A form will appear and prompt you to insert the following data:

  • Activity Name: An arbitrary name that will be used within CampaignChain. For example, “Initial announcement”.
  • Twitter Message: This is the text that will appear on Twitter, e.g. “Try our new product, it’s awesome: http://www.example.com/newproduct”
  • Due: Here, you can schedule the tweet to be posted at a specific date and time.
  • Assignee: Define who is responsible for taking care of this Tweet.
_images/new_twitter_status_update_form.png

That’s it! If you now click Plan again, you will see the new Activity as part of your new Campaign.

Miscellanea

Contributing

Contributing to Documentation

CampaignChain Documentation Standards
Symfony Documentation Standards

CampaignChain documentation follows the documentation standards of the Symfony project.

Additional Conventions

CampaignChain documentation also follows these additional conventions.

  1. When referring to an CampaignChain entity, you should start the entity name with a capital letter. For example, you should use “Channel module” instead of “channel module”. This makes it clear that you are referring to a specific entity and not a generic channel, location, activity, operation, hook or milestone concept.
  2. All file names and directory paths are italicized. You should always include a trailing slash on directory paths. For example, public/images/icons/16x16/linkedin.png refers to a file name and your-project/src/ refers to a directory path.
  3. When referring to key or parameters names in configuration files, those names should be italicized. However, when referring to the corresponding values, you should enclose those values in single quotes. For example, the parameter foo and the value ‘bar’.
  4. CampaignChain documentation favors using :: shorthand for PHP code blocks and code-block:: syntax for other code blocks. For example:
::
  <?php
  // PHP code block
  $wizard = $this->get('campaignchain.core.channel.wizard');
  $wizard->setName($profile->displayName);
  $wizard->addLocation($location->getIdentifier(), $location);
  $channel = $wizard->persist();
  $wizard->end();

.. code-block::yaml

  # YAML code block
  acme_campaignchain_channel_linkedin_create:
    pattern:  /channel/linkedin/create
    defaults: { _controller: AcmeCampaignChainChannelLinkedInBundle:LinkedIn:create }
  1. CampaignChain documentation calls out pieces of key information using note:: and tip:: specific admonitions. For example:
.. note::
   This is something important.
  1. Images should be created in LibreOffice Draw. Please provide the original .odg files in the /images/ directory along with the related PNG file.
Documentation License

The CampaignChain documentation is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.

You are free:

  • to Share — to copy, distribute and transmit the work;
  • to Remix — to adapt the work.

Under the following conditions:

  • Attribution — You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work);
  • Share Alike — If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one.

With the understanding that:

  • Waiver — Any of the above conditions can be waived if you get permission from the copyright holder;
  • Public Domain — Where the work or any of its elements is in the public domain under applicable law, that status is in no way affected by the license;
  • Other Rights — In no way are any of the following rights affected by the license:
    • Your fair dealing or fair use rights, or other applicable copyright exceptions and limitations;
    • The author’s moral rights;
    • Rights other persons may have either in the work itself or in how the work is used, such as publicity or privacy rights.
  • Notice — For any reuse or distribution, you must make clear to others the license terms of this work. The best way to do this is with a link to this web page.

This is a human-readable summary of the Legal Code (the full license).

Glossary

Activity
Every location allows one or more Activities which can be undertaken. For example, creating a new post is an example of an Activity for a blog Channel.
Campaign
Campaigns are a series of marketing operations utilizing one or more communication channels. Campaigns have Milestones and Activities.
Channel
Campaigns use online channels, which are the pathways by which campaign content reaches its audience. Common examples of Channels include websites, blogs and social networks like Facebook and LinkedIn.
CTA
Call to Action
GUI
Graphical User Interface
Hook
A Hook is a reusable component that provide common functionality and can be used across modules to configure Campaigns, Milestones, Channels, Locations, Activities and Operations.
Location
Every Channel includes one or more Locations, which allow granular publishing of campaign content. For example, for a Twitter Channel, the Twitter stream is a Location.
Milestone
Milestones are key events or reference points during a Campaign. For example, the campaign go-live date could be a Milestone, and a press tour could be a second Milestone.
Module
A module is a pre-packaged set of functionality. In CampaignChain, modules are developed as Symfony bundles, with additional configuration that allows CampaignChain to load them into its system.
Operation
Every Activity must always have at least one Operation. For example, posting on Twitter is one Activity which equals the Operation.
Tracking ID
Each Call to Action has a unique Tracking ID assigned by CampaignChain which enables CampaignChain to match a URL clicked in a Channel to a Location specified inside CampaignChain.

Documentation License

The CampaignChain documentation is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.

You are free:

  • to Share — to copy, distribute and transmit the work;
  • to Remix — to adapt the work.

Under the following conditions:

  • Attribution — You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work);
  • Share Alike — If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one.

With the understanding that:

  • Waiver — Any of the above conditions can be waived if you get permission from the copyright holder;
  • Public Domain — Where the work or any of its elements is in the public domain under applicable law, that status is in no way affected by the license;
  • Other Rights — In no way are any of the following rights affected by the license:
    • Your fair dealing or fair use rights, or other applicable copyright exceptions and limitations;
    • The author’s moral rights;
    • Rights other persons may have either in the work itself or in how the work is used, such as publicity or privacy rights.
  • Notice — For any reuse or distribution, you must make clear to others the license terms of this work. The best way to do this is with a link to this web page.

This is a human-readable summary of the Legal Code (the full license).