I would like to have Silverstripe's userforms add on submit values to a CRM, namely Active Campaign.
I see there is a SubmittedForm class but how would I go about taking the data before it is submitted and submit it via my API as well?
Alternatively, is there a way that once the form data has been submitted to the DB it redirects to a custom page with the ID of the form that has just been submitted? Then I can grab the fields and submit to the API.
You can retrieve submitted form data from a specific UserDefinedForm page like this:
/** #var SubmittedForm[] $submissions */
$submissions = UserDefinedForm::get()->filter(['URLSegment' => 'my-integrated-form'])->Submissions();
Or retrieve all submissions for all user form pages:
$submissions = SubmittedForm::get();
From here to integrate your data to an external system you could write a BuildTask that does what you need to do. You could plug this into the crontask module, or the queuedjobs module to have it run on a schedule daily:
class IntegrateUserFormSubmissionsTask extends BuildTask
{
protected $title = 'Integrate userforms submissions somewhere';
protected $api;
public function run($request)
{
$submissions = SubmittedForm::get();
foreach ($submissions as $submission) {
$formatted = [
'user' => [
'first_name' => $submission->SubmittedBy()->FirstName,
'surname' => $submission->SubmittedBy()->Surname,
],
// ...
];
$this->getYourApi()->submitData($formatted);
}
}
public function getYourApi()
{
if (!$this->api) {
$this->api = new RestfulService(...);
}
return $this->api;
}
}
Of course this is just a quick example, you'd need to map your data structures. Take a look at SubmittedForm for the actual submission structure, and note that most of the actual form field values will be under $submission->Values(). You could use the SubmittedFormField::getExportValue method to return something useful for all "values".
Note that I'd suggest strongly that you don't run this integration inline with extensions to the user defined form, since that will introduce a synchronous dependency which could prevent your forms from working if the other server went down, you experience network latency etc.
If you need a way of monitoring which records have been exported you could use a DataExtension to provide an "ExportedTimestamp" field or something which you could set when you've successfully sent the data to your API:
public function ExternalApiExtension extends DataExtension
{
private static $db = [
'LastExported' => 'SS_Datetime',
];
public function markAsExported()
{
$this->owner->LastExported = SS_Datetime::now();
$this->owner->write();
}
}
Apply the extension to SubmittedForm with YAML configuration (plenty in the docs about this), then in your export BuildTask you can just add $submission->markAsExported(); when you've validated a successful result from your API. Include ->filter(['LastExported' => null]) or something in the filter for your DataList before you run through all your records.
Good luck!
I think you should be able to add a DataExtension to SubmittedForm that implements updateAfterProcess.
It's an extension hook on SubmittedForm that will be called once all user-data has been validated and the email (if applicable) has been sent.
Your extension could look like this:
<?php
class UserFormCRMExtension extends DataExtension
{
public function updateAfterProcess()
{
/** #var SubmittedFormField $field */
foreach ($this->owner->Values() as $field) {
/* prepare data for submittal to CRM */
}
/* send data to CRM */
}
}
Apply as usual, via config.yml :
SubmittedForm:
extensions:
- UserFormCRMExtension
You could go with extending the class and have it submitted manually, based on a certain prerequisite.
Same for redirection, you'll need to write an Extension.
Related
So I have a complicated onboarding process that does several steps. I created a class that handles the process but I've added a few more steps and I'd like to refactor this into something a bit more manageable. I refactored to use Laravel's pipeline, but feel this may not be the best refactor due to the output needing to be modified before each step.
Here is an example before and after with some pseudo code.
before
class OnboardingClass {
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
// Step 1
$user = User::create();
// Step 2
$conversation = Conversation::create(); // store information for new user + existing user
// Step 3
$conversation->messages()->create(); // store a message on the conversation
// Step 4
// Send api request to analytics
// Step 5
// Send api request to other service
return $this;
}
}
after
class OnboardingClass{
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
$data = ['first_name' => $firstName, ...]; // form data
$pipeline = app(Pipeline::Class);
$pipeline->send($data)
->through([
CreateUser::class,
CreateNewUserConversation::class,
AddWelcomeMessageToConversation::class,
...
])->then(function($data){
// set all properties returned from last class in pipeline.
$this->user = $data['user'];
$this->conversation = $data['conversation'];
});
return $this;
}
}
Now within each class I modify the previous data and output a modified version something like this
class CreateUser implements Pipe {
public function handle($data, Closure $next) {
// do some stuff
$user = User::create():
return $next([
'user' => $user,
'other' => 'something else'
]);
}
}
In my controller I am simply calling the create method.
class someController() {
public function store($request){
$onboarding = app(OnboardingClass::class);
$onboarding->create('John', 'Doe', 'john#example.com');
}
}
So the first pipe receives the raw form fields and outputs what the second pipe needs to get the job done in its class, then the next class outputs the data required by the next class, so on and so forth. The data that comes into each pipe is not the same each time and you cannot modify the order.
Feels a bit weird and I'm sure there is a cleaner way to handle this.
Any design pattern I can utilize to clean this up a bit?
I think you could try using Laravel Service Provider, for example, you could build a login service provider; or Event & Listener, for example, you could build an listener for login and triggers a event to handle all the necessary logics. Can't really tell which one is the best since outcome is the same and it makes same amount of network requests, but it's more on personal preferences
Is possible to attach a custom attribute when retrieving a model in laravel?.
The problem is that I need to return some data that is not in the database along the info from the database. I've been doing it manually but I guess that there might be a way to do it in the model.
Example: I have an application table. Each application contains a folder with documents with the same application id. I need to attach the amount of files the folder that correspond to each application.
This is what I do:
$application = Application::get();
$application = $application->map(function($a){
$a->files = $this->getFiles($a->id); // This gets the amount of files
return $a;
})
Is there some way to do it in the model in a way that $application->files is already contained in $application when doing Application::get()
class User extends Model
{
public function getFooBarAttribute()
{
return "foobar";
}
}
And access to that attribute like:
$user->foo_bar;
or like,
$user->fooBar;
More detailed documentation;
https://laravel.com/docs/5.7/eloquent-mutators#defining-an-accessor
in the Application model
public function getFilesAttribute()
{
return 'lala'; // return whatever you need;
}
now application model has an attribute named files.
$application->files // returns lala.
example code.
$applications = Application::get();
$application_files = applications->map->files;
official documentation https://laravel.com/docs/5.7/eloquent-mutators#defining-an-accessor
I have been using Behat for a year or so at a level fine for the automation of most websites but I now need to start using it more for user generated content, I am relatively new to PHP and at the moment I am struggling how to use a String entered in an Example table in an x-path array:
Feature: Campaign
Scenario Outline: Pass campaign string to xpath array
Then I add a new campaign name of "<campaign>"
Examples:
|campaign |
|Automation|
The context file looks like this
/**
* #Then /^I add a new campaign name of "([^"]*)"$/
*/
public function iAddANewCampaignNameOf($campaign)
{
/**
* #var CreateCampaign $createCampaign
*/
$createCampaign= $this->getPage('CreateCampaign');
$createCampaign->campaignName($campaign);
}
Then I use the Page Object extension for the class Campaign.php
class CreateCampaign extends AutomationPage
{
protected $path = 'someURL';
public $campaign;
protected $elements = array(
'campaignHeader' => array('xpath' => "//*[#id='site-navigation-campaigns']"),
);
public function campaignName ($campaign)
{
$this->campaign = $campaign;
$this->getSession()->wait(5000);
$this->getElement('campaignName')->setValue($campaign);
}
So far so good, the tester can enter a campaign name of "Automation" - it gets passed through the context file and the campaign name is set in the browser.
What I am lacking is to be able to retain this $campaign name string and use it in another page so I can reference it in another array i.e. for selecting an existing campaign as follows:
SecondPageObjectPage.php
class ReferenceCampaign extends AutomationPage
{
protected $path = 'someURL';
protected $elements = array(
'referenceCampaign' => array('xpath' => "//*[contains(#id,'***HERE I NEED TO GET THE
$campaign value"),
);
public function editExistingCampaign ($campaign)
{
$this->getElement('referenceCampaign')->click();
}
}
I have tried my best to simplify things and I can explain further if any of this isnt clear - hopefully its just a simple PHP question and not really Behat specific
Thanks Ian
Your example is a much better way of doing things, I have only recently started using partial contains and it expands the flexibility of finding stubborn xpaths especially if you combine more than one, like the working example below:
public function editExistingCampaign ($campaign)
{
$this->getSession()->wait(5000);
$element = $this->find('xpath', '//*[contains(#id,"'.$campaign.'")]
[contains(#id,"actionbuttons")]');
if (isset($element)) {
$element->click();
} else {
throw new Exception('Element not found');
}
}
The only slight change was to add a ] at the end of the x-path
I'm sure it's a simple question, but I think that I am missing a point. If all you want is to get hold of the value that was used on the page then you need to review your code structure. First, you cannot pass method argument to the property definition in another class, but you can find the element inside editExistingCampaign.
class ReferenceCampaign extends AutomationPage
{
protected $path = 'someURL';
public function editExistingCampaign ($campaign)
{
$element = $this->find('xpath', '//*[contains(#id, "' . $campaign . '")]');
if (isset($element)) {
$element->click();
} else {
throw new Exception('Element not found');
}
}
}
I'm assuming you are using Symfony Page Object extension, which you should mention. I'm not sure if I've got the syntax right, but the idea is to find your element inside the method.
I am trying to follow a tutorial for Zend Auth and Zend Acl using 1.11 framework Link here!
I have setup the authentication successfully and am able to use the authentication for the controller::action pairs given in the Acl.php page. Firstly I would like to test two additional parameter on the users table that whether the user account is activated and if the user is banned by administrator before allowing access to the site. How do I implement that in this code.
Secondly I would like to know how to include all actions under one controller to a User authorization level. i.e. I have a masters controller which has numerous actions under it for various tables. Could you tell me how to restrict access to Masters controller all actions to admin role only. Without adding resources and allow resources for each action in Acl.php. Also please tell me if this logic can be extended to allow access over entire modules instead of just the controllers(by one add resource and allow resource)? If yes how?
Firstly I would like to test two additional parameter on the users
table that whether the user account is activated and if the user is
banned by administrator before allowing access to the site.
The tutorial code uses a vanilla version of Zend_Auth_Adapter_DbTable which uses a specific api for authentication. To make Zend_Auth work how you want it to is not very difficult but will require some thought as you'll need to implement Zend_Auth_Adapter_Interface. Sounds worse then it is, you only have to implement the authenticate() method. Here is an example of an auth adapter that can be used in place of Zend_Auth_Adapter_DbTable:
<?php
//some code truncated for length and relevance
class My_Auth_Adapter implements Zend_Auth_Adapter_Interface
{
protected $identity = null;
protected $credential = null;
protected $usersMapper = null;
public function __construct($username, $password, My_Model_Mapper_Abstract $userMapper = null)
{
if (!is_null($userMapper)) {
$this->setMapper($userMapper);
} else {
$this->usersMapper = new Users_Model_Mapper_User();
}
$this->setIdentity($username);
$this->setCredential($password);
}
/**
* #return \Zend_Auth_Result
*/
public function authenticate()
{
// Fetch user information according to username
$user = $this->getUserObject();
if (is_null($user)) {
return new Zend_Auth_Result(
Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND,
$this->getIdentity(),
array('Invalid username')
);
}
// check whether or not the hash matches
$check = Password::comparePassword($this->getCredential(), $user->password);
if (!$check) {
return new Zend_Auth_Result(
Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID,
$this->getIdentity(),
array('Incorrect password')
);
}
// Success!
return new Zend_Auth_Result(
Zend_Auth_Result::SUCCESS,
$this->getIdentity(),
array()
);
}
// public function setIdentity($userName)
// public function setCredential($password)
// public function setMapper($mapper)
/**
* #return object
*/
private function getUserObject()
{
return $this->getMapper()->findOneByColumn('username', $this->getIdentity());
}
/**
* #return object
*/
public function getUser()
{
$object = $this->getUserObject();
$array = array(
'id' => $object->id,
'username' => $object->username,
'role' => $object->getRoleId()
);
return (object) $array;
}
// public function getIdentity()
// public function getCredential()
// public function getMapper()
}
You can modify the auth adapter to do pretty much anything you need.
As far as your access list is concerned, the thing to remember is that you resources are defined by a string. In the case of this tutorial a resource is defined as:
$this->add(new Zend_Acl_Resource('error::error'));
where the string on the left side of the colon represents the controller and the string on the right side of the colon represents the action. it's this line in the acl plugin that tell's us what the resources are:
if(!$acl->isAllowed($user->role, $request->getControllerName() . '::' . $request->getActionName()))
you can change this definition of what your resources represent to anything that works for you.
It's very difficult to provide hard and fast rules on how to implement an ACL because it seems that every project needs something different.
Look around the web and you'll find several different implementations of a Zend Framework ACL, some of them can be very complex.
Here is one that might provide some more insight. http://codeutopia.net/blog/2009/02/06/zend_acl-part-1-misconceptions-and-simple-acls/
good luck
I have build a CMS using Zend Framework (1.11). In the application I have two modules, one called 'cms' which contains all the CMS logic and an other 'web' which enables a user to build their own website around the CMS. This involves adding controllers/views/models etc all in that module.
The application allows you to serve multiple instances of the app with their own themes. These instances are identified by the hostname. During preDispatch(), a database lookup is done on the hostname. Based on the database field 'theme' the app then loads the required css files and calls Zend_Layout::setLayout() to change the layout file for that specific instance.
I want to extend this functionality to also allow the user to run different web modules based on the 'theme' db field. However, this is where I am stuck. As it is now, the web module serves the content for all the instances of the application.
I need the application to switch to a different web module based on the 'theme' database value (so indirectly the hostname). Any ideas?
Well, in my opinion,
You should write a front controller plugin for the web module, and do it so, that when you need another plugin, you can do so easily.
The front controller plugin should look something like this:
class My_Controller_Plugin_Web extends My_Controller_Plugin_Abstract implements My_Controller_Plugin_Interface
{
public function init()
{
// If user is not logged in - send him to login page
if(!Zend_Auth::getInstance()->hasIdentity()) {
// Do something
return false;
} else {
// You then take the domain name
$domainName = $this->_request->getParam( 'domainName', null );
// Retrieve the module name from the database
$moduleName = Module_fetcher::getModuleName( $domainName );
// And set the module name of the request
$this->_request->setModuleName( $moduleName );
if(!$this->_request->isDispatched()) {
// Good place to alter the route of the request further
// the way you want, if you want
$this->_request->setControllerName( $someController );
$this->_request->setActionName( $someAction );
$this->setLayout( $someLayout );
}
}
}
/**
* Set up layout
*/
public function setLayout( $layout )
{
$layout = Zend_Layout::getMvcInstance();
$layout->setLayout( $layout );
}
}
And the My_Controller_Plugin_Abstract, which is not an actual abstract and which your controller plugin extends,looks like this:
class My_Controller_Plugin_Abstract
{
protected $_request;
public function __construct($request)
{
$this->setRequest($request);
$this->init();
}
private function setRequest($request)
{
$this->_request = $request;
}
}
And in the end the front controller plugin itself, which decides which one of the specific front controller plugins you should execute.
require_once 'Zend/Controller/Plugin/Abstract.php';
require_once 'Zend/Locale.php';
require_once 'Zend/Translate.php';
class My_Controller_Plugin extends Zend_Controller_Plugin_Abstract
{
/**
* before dispatch set up module/controller/action
*
* #param Zend_Controller_Request_Abstract $request
*/
public function routeShutdown(Zend_Controller_Request_Abstract $request)
{
// Make sure you get something
if(is_null($this->_request->getModuleName())) $this->_request->setModuleName('web');
// You should use zend - to camelCase convertor when doing things like this
$zendFilter = new Zend_Filter_Word_SeparatorToCamelCase('-');
$pluginClass = 'My_Controller_Plugin_'
. $zendFilter->filter($this->_request->getModuleName());
// Check if it exists
if(!class_exists($pluginClass)) {
throw new Exception('The front controller plugin class "'
. $pluginClass. ' does not exist');
}
Check if it is written correctly
if(!in_array('My_Controller_Plugin_Interface', class_implements($pluginClass))) {
throw new Exception('The front controller plugin class "'
. $pluginClass.'" must implement My_Controller_Plugin_Interface');
}
// If all is well instantiate it , and you will call the construct of the
// quasi - abstract , which will then call the init method of your
// My_Plugin_Controller_Web :)
$specificController = new $pluginClass($this->_request);
}
}
If you have never done this, now is the time. :)
Also, to register your front controller plugin with the application, you should edit the frontController entry in your app configuration. I will give you a json example, i'm sure you can translate it to ini / xml / yaml if you need...
"frontController": {
"moduleDirectory": "APPLICATION_PATH/modules",
"defaultModule": "web",
"modules[]": "",
"layout": "layout",
"layoutPath": "APPLICATION_PATH/layouts/scripts",
// This is the part where you register it!
"plugins": {
"plugin": "My_Controller_Plugin"
}
This might be a tad confusing, feel free to ask for a more detailed explanation if you need it.