Skip to content

Instantly share code, notes, and snippets.

@hovsep
Created May 11, 2017 17:09
Show Gist options
  • Save hovsep/82c0ef61afc7308f3b1fb74112cc89fb to your computer and use it in GitHub Desktop.
Save hovsep/82c0ef61afc7308f3b1fb74112cc89fb to your computer and use it in GitHub Desktop.
Laravel 5.4 code example
<?php
namespace App\Http\Controllers;
use App\Entities\CampaignState;
use App\Entities\EventAction;
use App\Entities\GroupScope;
use App\Entities\SubjectMode;
use App\Facades\BladeStringRenderer;
use App\Facades\Mail;
use App\Http\Controllers\Traits\RBAC;
use App\Jobs\EnqueueStatEvent;
use App\Models\Campaign;
use App\Models\CampaignDynamicSubject;
use App\Models\CampaignHeader;
use App\Models\CampaignVariable;
use App\Models\EmailTransport;
use App\Models\Group;
use App\Models\RBAC\Permission;
use App\Models\RecipientList;
use App\Models\Settings;
use App\Models\Template;
use App\Models\TrackingCategory;
use App\Utils\Campaign\Manager as CampaignManager;
use Illuminate\Http\Request;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\View;
use Illuminate\Support\MessageBag;
use Triton\Entities\Recipient\RecipientField;
use Triton\Entities\Variable\Reserved;
use Triton\Entities\Variable\VariableField;
class CampaignsController extends Controller {
use RBAC;
public function __construct()
{
$this->setupAccessControll([
Permission::CAMPAIGN_CREATE => ['create', 'store', 'showImportForm', 'import'],
Permission::CAMPAIGN_EDIT => ['edit', 'update', 'run', 'stop'],
Permission::CAMPAIGN_DELETE => ['destroy'],
Permission::CAMPAIGN_TEST => ['test']
]);
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$groupId = $request->get('group_id', Group::ANY);
if ($groupId === Group::ANY) {
$campaigns = Campaign::orderBy('id')->paginate(Settings::get('campaigns_per_page', 50));
} else {
$groupId = (int) $groupId;
$campaigns = Campaign::where('group_id', $groupId)->orderBy('id')->paginate(Settings::get('campaigns_per_page', 50));
}
//Store state
Session::put('campaigns.getParams', [
'page' => Paginator::resolveCurrentPage(),
'group_id' => $groupId
]);
return response(view('campaigns.list', [
'campaigns' => $campaigns,
'groupsOptions' => Group::getOptions(GroupScope::CAMPAIGNS, [0 => 'Not assigned', Group::ANY => 'Any']),
'groupId' => $groupId //Selected group filter
])->render(), 200, [
'Cache-Control' => 'private, must-revalidate,max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0',
'Pragma' => 'no-cache'
]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
$listOptions = RecipientList::getOptions();
$templateOptions = Template::getOptions();
if (empty($listOptions)) {
return $this->noLists();
}
if (empty($templateOptions)) {
return $this->noTemplates();
}
$groupId = Session::get('campaigns.getParams.group_id', 0);
return view('campaigns.create', [
'campaign' => new Campaign(),
'lists' => $listOptions,
'templates' => $templateOptions,
'trackingOptions' => TrackingCategory::getOptions([0 => 'No tracking']),
'transportOptions' => EmailTransport::getOptions(),
'groupsOptions' => Group::getOptions(GroupScope::CAMPAIGNS, [0 => 'Not assigned']),
'groupId' => ($groupId != Group::ANY) ? $groupId : 0//Preset current group filter
]
);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$fields = [
'name' => 'required|string|max:100|unique:campaigns,name',
'template_id' => 'required',
'list_id' => 'required',
'subjectMode' => 'required|in:' . implode(',', SubjectMode::all()),
'subject' => 'required_if:subjectMode,' . SubjectMode::SINGLE,
'description' => 'nullable|string|max:2000',
'tracking_category_id' => 'integer|in:' . implode(',', array_keys(TrackingCategory::getOptions([0 => 'No tracking']))),
'group_id' => 'integer|in:' . implode(',', array_keys(Group::getOptions(GroupScope::CAMPAIGNS, [0 => 'Not assigned']))),
'transport_id' => 'string|in:' . implode(',', array_keys(EmailTransport::getOptions())),
'scheduled_at' => 'nullable|date|after:now'
];
$validationMessages = [];
###CUSTOM HEADERS VALIDATION RULES START
$customHeadersCount = (int) $request->get('customHeadersCount', 0);
$usedHeaderNames = [];//Need for validation
if ((bool) $request->get('useCustomHeaders') && ($customHeadersCount > 0)) {
for($i=0; $i < $customHeadersCount; $i++) {
$fields["customHeaderName_$i"] = 'required|string';
$validationMessages["customHeaderName_$i.required"] = 'Header name is required';
if (!empty($usedHeaderNames)) {
$fields["customHeaderName_$i"] .= '|not_in:' . implode(',', $usedHeaderNames);
$validationMessages["customHeaderName_$i.not_in"] = 'Header name already used';
}
$usedHeaderNames[] = strtolower($request->get("customHeaderName_$i"));
$fields["customHeaderValue_$i"] = 'required|string';
$validationMessages["customHeaderValue_$i.required"] = 'Header value is required';
}
}
###CUSTOM HEADERS VALIDATION RULES END
###CUSTOM VARIABLES VALIDATION RULES START
$reservedVarNames = Reserved::all();
$list = RecipientList::find((int) $request->get('list_id'));
if (empty($list)) {
throw new \Exception('List not found');
}
$listColumnNames = $list->getDataTableColumns();
$customVariablesCount = (int) $request->get('customVariablesCount', 0);
$usedVariablesNames = [];//Need for validation
if ((bool) $request->get('useCustomVariables') && ($customVariablesCount > 0)) {
for($i = 0; $i < $customVariablesCount; $i++) {
$fields["customVariableName_$i"] = 'required|string|regex:~^[a-zA-Z_][a-zA-Z0-9_]*$~|not_in:' . implode(',', array_merge($usedVariablesNames, $reservedVarNames, $listColumnNames));
$validationMessages["customVariableName_$i.required"] = 'Variable name is required';
$validationMessages["customVariableName_$i.regex"] = 'Variable name is incorrect';
$validationMessages["customVariableName_$i.not_in"] = 'Variable name reserved or used in list. Please try another name';
$usedVariablesNames[] = strtolower($request->get("customVariableName_$i"));
$fields["customVariableValue_$i"] = 'required|string';
$validationMessages["customVariableValue_$i.required"] = 'Variable value is required';
}
}
###CUSTOM VARIABLES VALIDATION RULES END
###DYNAMIC SUBJECTS VALIDATION RULES START
$dynamicSubjectsCount = (int) $request->get('dynamicSubjectsCount', 0);
$dynamicSubjects = [];
if (SubjectMode::isDynamic($request->get('subjectMode')) && $dynamicSubjectsCount > 0) {
for($i = 0; $i < $dynamicSubjectsCount; $i++) {
$fields["dynamicSubject_$i"] = 'required|string';
$validationMessages["dynamicSubject_$i.required"] = 'Subject can not be empty';
$dynamicSubjects["dynamicSubject_$i"] = $request->get("dynamicSubject_$i");
}
}
###DYNAMIC SUBJECTS VALIDATION RULES END
$values = [];
foreach ($fields as $field => $rules) {
$values[$field] = $request->get($field);
if ((false !== strpos($field, 'customHeaderName_')) || (false !== strpos($field, 'customVariableName_'))) {
$values[$field] = strtolower($values[$field]);
}
}
unset($field);
unset($rules);
/* @var $validator Validator */
$validator = Validator::make($values, $fields, $validationMessages);
try {
if ($validator->fails()) {
throw new \Exception('Validation failed');
}
$campaign = new Campaign($values);
$campaign->setState(CampaignState::IDLE);
###SUBJECTS_VALIDATION_START
$context = $campaign->getTestContext();
$subjectScopeVariables = array_map(function($item) {return $item[VariableField::TEST_VALUE];}, $context);
View::share($subjectScopeVariables);
if ($campaign->subjectMode == SubjectMode::SINGLE) {
try {
$rendered = BladeStringRenderer::render($campaign->subject);
} catch (\Exception $e) {
$validator->errors()->add('subject', 'Failed to render subject. Reason: ' . $e->getMessage());
throw $e;
}
} elseif (SubjectMode::isDynamic($campaign->subjectMode)) {
if (count($dynamicSubjects) <= 1) {
$validator->errors()->add('tooFewSubjects', 'Please define at least 2 subjects');
throw new \Exception('Dynamic mode does not make sense if you have single subject');
}
$dynamicSubjectsRenderingFail = false;
foreach($dynamicSubjects as $field => $ds) {
try {
$rendered = BladeStringRenderer::render($ds);
} catch (\Exception $e) {
$validator->errors()->add($field, 'Failed to render subject. Reason: ' . $e->getMessage());
$dynamicSubjectsRenderingFail = true;// Do not throw exception here to collect each subject rendering errors
}
}
//Throw validation exception after all subjects are validated
if ($dynamicSubjectsRenderingFail) {
throw new \Exception('Some dynamic subjects may not be rendered. Please check');
}
unset($field);
}
###SUBJECTS_VALIDATION_END
$campaign->save();
//Save subject(s)
if ($campaign->subjectMode == SubjectMode::SINGLE) {
//Subject already saved within campaign record
} elseif (SubjectMode::isDynamic($campaign->subjectMode)) {
$subjectOrder = 1;
foreach($dynamicSubjects as $field => $ds) {
$dSubject = new CampaignDynamicSubject(['campaign_id' => $campaign->id, 'value' => $ds]);
if (SubjectMode::SEQUENTIAL == $campaign->subjectMode) {
$nextSubjectOrder = ($subjectOrder === count($dynamicSubjects)) ? 1 : ($subjectOrder + 1);
$dSubject->order = $subjectOrder;
$dSubject->next = $nextSubjectOrder;
$subjectOrder++;
}
$dSubject->save();
}
}
//Save headers
for ($i = 0; $i < $customHeadersCount; $i++) {
$header = new CampaignHeader(['campaign_id' => $campaign->id, 'name' => $values["customHeaderName_$i"], 'value' => $values["customHeaderValue_$i"]]);
$header->save();
}
//Save variables
for ($i = 0; $i < $customVariablesCount; $i++) {
$variable = new CampaignVariable(['campaign_id' => $campaign->id, 'name' => $values["customVariableName_$i"], 'value' => $values["customVariableValue_$i"]]);
$variable->save();
}
return Redirect::route('campaigns.index', Session::get('campaign.getParams'))->withMessage('Campaign created !');
} catch (\Exception $e) {
return Redirect::to('campaigns/create')->withErrors($validator)->withError('Failed to store campaign. Reason: ' . $e->getMessage())->withInput();
}
}
/**
* Show the form for editing the specified resource.
*
* @param $id
* @return \Illuminate\Contracts\View\Factory|View|\Illuminate\View\View
*/
public function edit($id)
{
try {
if (empty($id)) {
throw new \Exception('Empty id received');
}
$campaign = Campaign::find($id);
if ($campaign->getState() == CampaignState::RUNNING) {
throw new \Exception('You can not modify running campaign');
}
$listsOptions = RecipientList::getOptions();
$templateOptions = Template::getOptions();
if (empty($listsOptions)) {
return $this->noLists();
}
if (empty($templateOptions)) {
return $this->noTemplates();
}
if (empty($campaign)) {
throw new \Exception('Campaign not found');
}
$campaign->useCustomHeaders = (bool) (count($campaign->headers()) > 0);
$campaign->useCustomVariables = (bool) (count($campaign->variables()) > 0);
return view('campaigns.edit', [
'campaign' => $campaign,
'lists' => $listsOptions,
'templates' => $templateOptions,
'trackingOptions' => TrackingCategory::getOptions([0 => 'No tracking']),
'transportOptions' => EmailTransport::getOptions(),
'groupsOptions' => Group::getOptions(GroupScope::CAMPAIGNS, [0 => 'Not assigned'])
]);
} catch (\Exception $e){
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withError('Failed to edit campaign. Reason: ' . $e->getMessage());
}
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
try {
if (empty($id)) {
throw new \Exception('Empty id received');
}
$campaign = Campaign::find($id);
if (empty($campaign)) {
throw new \Exception('Campaign not found');
}
$fields = [
'name' => "required|string|max:100|unique:campaigns,name,$id",
'template_id' => 'required',
'list_id' => 'required',
'subjectMode' => 'required|in:' . implode(',', SubjectMode::all()),
'subject' => 'required_if:subjectMode,' . SubjectMode::SINGLE,
'description' => 'nullable|string|max:2000',
'tracking_category_id' => 'integer|in:' . implode(',', array_keys(TrackingCategory::getOptions([0 => 'No tracking']))),
'group_id' => 'integer|in:' . implode(',', array_keys(Group::getOptions(GroupScope::CAMPAIGNS, [0 => 'Not assigned']))),
'transport_id' => 'string|in:' . implode(',', array_keys(EmailTransport::getOptions())),
'scheduled_at' => 'nullable|date|after:now'
];
$validationMessages = [];
###CUSTOM HEADERS VALIDATION RULES START
$customHeadersCount = (int) $request->get('customHeadersCount', 0);
$usedHeaderNames = [];//Need for validation
if ((bool) $request->get('useCustomHeaders', false) && ($customHeadersCount > 0)) {
for($i = 0; $i < $customHeadersCount; $i++) {
$fields["customHeaderName_$i"] = 'required|string';
$validationMessages["customHeaderName_$i.required"] = 'Header name is required';
if (!empty($usedHeaderNames)) {
$fields["customHeaderName_$i"] .= '|not_in:' . implode(',', $usedHeaderNames);
$validationMessages["customHeaderName_$i.not_in"] = 'Header name already used';
}
$usedHeaderNames[] = strtolower($request->get("customHeaderName_$i"));
$fields["customHeaderValue_$i"] = 'required|string';
$validationMessages["customHeaderValue_$i.required"] = 'Header value is required';
}
}
###CUSTOM HEADERS VALIDATION RULES END
###CUSTOM VARIABLES VALIDATION RULES START
$reservedVarNames = Reserved::all();
$list = RecipientList::find((int) $request->get('list_id'));
if (empty($list)) {
throw new \Exception('List not found');
}
$listColumnNames = $list->getDataTableColumns();
$customVariablesCount = (int) $request->get('customVariablesCount', 0);
$usedVariablesNames = [];//Need for validation
if ((bool) $request->get('useCustomVariables', false) && ($customVariablesCount > 0)) {
for($i=0; $i < $customVariablesCount; $i++) {
$fields["customVariableName_$i"] = 'required|string|regex:~^[a-zA-Z_][a-zA-Z0-9_]*$~|not_in:' . implode(',', array_merge($usedVariablesNames, $reservedVarNames, $listColumnNames));
$validationMessages["customVariableName_$i.required"] = 'Variable name is required';
$validationMessages["customVariableName_$i.regex"] = 'Variable name is incorrect';
$validationMessages["customVariableName_$i.not_in"] = 'Variable name already reserved. Please try another name';
$usedVariablesNames[] = strtolower($request->get("customVariableName_$i"));
$fields["customVariableValue_$i"] = 'required|string';
$validationMessages["customVariableValue_$i.required"] = 'Variable value is required';
}
}
###CUSTOM VARIABLES VALIDATION RULES END
###DYNAMIC SUBJECTS VALIDATION RULES START
$dynamicSubjectsCount = (int) $request->get('dynamicSubjectsCount', 0);
$dynamicSubjects = [];
if (SubjectMode::isDynamic($request->get('subjectMode')) && $dynamicSubjectsCount > 0) {
for($i = 0; $i < $dynamicSubjectsCount; $i++) {
$fields["dynamicSubject_$i"] = 'required|string';
$validationMessages["dynamicSubject_$i.required"] = 'Subject can not be empty';
$dynamicSubjects["dynamicSubject_$i"] = $request->get("dynamicSubject_$i");
}
}
###DYNAMIC SUBJECTS VALIDATION RULES END
$values = [];
foreach ($fields as $field => $rules) {
$values[$field] = $request->get($field);
if ((false !== strpos($field, 'customHeaderName_')) || (false !== strpos($field, 'customVariableName_'))) {
$values[$field] = strtolower($values[$field]);
}
}
unset($field);
unset($rules);
$validator = Validator::make($values, $fields, $validationMessages);
try {
if ($validator->fails()) {
throw new \Exception('Validation failed');
}
###SUBJECTS_VALIDATION_START
$context = $campaign->getTestContext();
$subjectScopeVariables = array_map(function($item) {return $item[VariableField::TEST_VALUE];}, $context);
View::share($subjectScopeVariables);
if ($values['subjectMode'] == SubjectMode::SINGLE) {
try {
$rendered = BladeStringRenderer::render($values['subject']);
} catch (\Exception $e) {
$validator->errors()->add('subject', 'Failed to render subject. Reason: ' . $e->getMessage());
throw $e;
}
} elseif (SubjectMode::isDynamic($values['subjectMode'])) {
if (count($dynamicSubjects) <= 1) {
$validator->errors()->add('tooFewSubjects', 'Please define at least 2 subjects');
throw new \Exception('Dynamic mode does not make sense if you have single subject');
}
$dynamicSubjectsRenderingFail = false;
foreach($dynamicSubjects as $field => $ds) {
try {
$rendered = BladeStringRenderer::render($ds);
} catch (\Exception $e) {
$validator->errors()->add($field, 'Failed to render subject. Reason: ' . $e->getMessage());
$dynamicSubjectsRenderingFail = true;// Do not throw exception here to collect each subject rendering errors
}
}
//Throw validation exception after all subjects are validated
if ($dynamicSubjectsRenderingFail) {
throw new \Exception('Some dynamic subjects has errors');
}
unset($field);
}
###SUBJECTS_VALIDATION_END
$campaign->update($values);
//Save subject(s)
$campaign->deleteAllDynamicSubjects();
if ($campaign->subjectMode == SubjectMode::SINGLE) {
//Subject already saved within campaign record
} elseif (SubjectMode::isDynamic($campaign->subjectMode)) {
$subjectOrder = 1;
foreach($dynamicSubjects as $field => $ds) {
$dSubject = new CampaignDynamicSubject(['campaign_id' => $campaign->id, 'value' => $ds]);
if (SubjectMode::SEQUENTIAL == $campaign->subjectMode) {
$nextSubjectOrder = ($subjectOrder === count($dynamicSubjects)) ? 1 : ($subjectOrder + 1);
$dSubject->order = $subjectOrder;
$dSubject->next = $nextSubjectOrder;
$subjectOrder++;
}
$dSubject->save();
}
}
//Update headers
$campaign->deleteAllHeaders();
for ($i = 0; $i < $customHeadersCount; $i++) {
$header = new CampaignHeader(['campaign_id' => $campaign->id, 'name' => $values["customHeaderName_$i"], 'value' => $values["customHeaderValue_$i"]]);
$header->save();
}
//Update variables
$campaign->deleteAllVariables();
for ($i = 0; $i < $customVariablesCount; $i++) {
$variable = new CampaignVariable(['campaign_id' => $campaign->id, 'name' => $values["customVariableName_$i"], 'value' => $values["customVariableValue_$i"]]);
$variable->save();
}
if ($request->get('saveMode', 'save') == 'save_and_exit') {
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withMessage("Campaign #$id successfully updated !");
} else {
return Redirect::action('CampaignsController@edit', ['id' => $id])->withMessage("Campaign #$id successfully updated !");
}
} catch (\Exception $e) {
return Redirect::action('CampaignsController@edit', ['id' => $id])->withErrors($validator)->withError('Failed to update campaign. Reason: ' . $e->getMessage())->withInput();
}
} catch (\Exception $e) {
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withError('Failed to update campaign. Reason: ' . $e->getMessage());
}
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
try {
if (empty($id)) {
throw new \Exception('Empty id received');
}
$campaign = Campaign::find($id);
if (empty($campaign)) {
throw new \Exception('Campaign not found');
}
if ($campaign->getState() == CampaignState::RUNNING) {
throw new \Exception('You can not delete running campaign');
}
$deletedCount = $campaign->delete();
if (empty($deletedCount)) {
throw new \Exception('DB error');
}
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withMessage("Campaign #$id successfully deleted !");
} catch (\Exception $e) {
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withError('Failed to delete campaign. Reason: ' . $e->getMessage());
}
}
/**
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function noLists()
{
return view('campaigns.no_lists');
}
/**
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function noTemplates()
{
return view('campaigns.no_templates');
}
/**
* Send or render test email
*/
public function test($id, Request $request)
{
$formFields = [
"testSubject_$id" => 'required',
"campaign_{$id}_testMethod" => 'required|in:send,render'
];
//Just for pretty validation messages
$formFieldNames = [
"testSubject_$id" => 'subject',
"campaign_{$id}_testMethod" => 'test method'
];
$formValues = [];
foreach ($formFields as $field => $rules) {
$formValues[$field] = $request->get($field);
}
unset($field);
unset($rules);
try {
$dialogValidator = Validator::make($formValues, $formFields);
$dialogValidator->setAttributeNames($formFieldNames);
/* @var $dialogValidator Validator */
if ($dialogValidator->passes()) {
$campaign = Campaign::find($id);
if (empty($campaign)) {
throw new \Exception('Campaign not found');
}
$testMethod = $formValues["campaign_{$id}_testMethod"];
$context = $campaign->getTestContext();
$variables = array_map(function($item) {return $item[VariableField::TEST_VALUE];}, $context);
//Override redefined variables
foreach ($variables as $varName => &$varValue) {
$redefined = $request->{"campaign_{$campaign->id}_test_" . $varName};
$varValue = empty($redefined) ? null : $redefined;
}
//Share them to make available at helper functions scope
View::share($variables);
//Validate & render subject
try {
$subject = BladeStringRenderer::render($formValues['testSubject_' . $id]);
} catch (\Exception $e) {
$dialogValidator->errors()->add('testSubject_' . $id, 'Incorrect syntax in subject');
throw new \Exception('Failed to render subject. Reason: ' . $e->getMessage());
}
try {
view()->addLocation(Config::get('triton.data_dir') . 'templates' . DIRECTORY_SEPARATOR);
$body = view($campaign->template->getViewName())->render();
} catch (\Exception $e) {
throw new \Exception('Failed to render template. Reason: ' . $e->getMessage());
}
if ('send' === $testMethod) {
Mail::send($subject, $body, $variables[RecipientField::EMAIL], $campaign->headersToArray(), $campaign->transport_id);
if (count(Mail::failures()) == 0) {
if ($campaign->trackingEnabled()) {
dispatch((new EnqueueStatEvent(
$campaign->trackingCategory->id,
$campaign->trackingCategory->name,
EventAction::SEND,
$variables[RecipientField::USER_ID],
$variables[RecipientField::CID])
)->onQueue(Config::get('queue.stats_queue_name')));
}
Session::flash('message', 'Mail successfully sent');
}
} elseif ('render' === $testMethod) {
return view('campaigns.test_render', ['subject' => $subject, 'body' => $body]);
}
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'));
} else {
throw new \Exception('Form validation failed');
}
} catch (\Exception $e) {
$dialogValidator->errors()->add('testDialogErrorCampaignId', $id);
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withErrors($dialogValidator)->with('testDialogError_' . $id, 'Oops! Something goes wrong. Details:' . $e->getMessage())->withInput();
}
}
/**
* Export campaign
*
* @param $id
* @return \Symfony\Component\HttpFoundation\StreamedResponse
*/
public function export($id) {
try {
if (empty($id)) {
throw new \Exception('Empty id received');
}
$campaign = Campaign::find($id);
if (empty($campaign)) {
throw new \Exception('Campaign not found');
}
return response()->tritonFile($campaign->getExportableData(), 'campaign', "campaign_{$campaign->id}_{$campaign->name}");
} catch (\Exception $e) {
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withError('Failed to export campaign. Reason: ' . $e->getMessage());
}
}
/**
* Import campaign from file
*
* @param Request $request
* @return mixed
*/
public function import(Request $request)
{
try {
$fileInfo = $request->file('campaignFile');
if (empty($fileInfo)) {
throw new \Exception('File is not uploaded');
}
if (!$fileInfo->isValid()) {
throw new \Exception($fileInfo->getErrorMessage());
}
if (!is_file($fileInfo->getPathname()) || !is_readable($fileInfo->getPathname())) {
throw new \Exception('Failed to read file');
}
$errors = new MessageBag();
$fileData = file_get_contents($fileInfo->getPathname(), FILE_TEXT);
//Check file
$securityToken = md5('campaign');
$fileData = base64_decode($fileData);
if (false === strpos($fileData, $securityToken)) {
throw new \Exception('Invalid or corrupted file');
}
$fileData = str_replace($securityToken, '', $fileData);
if (empty($fileData)) {
throw new \Exception('Empty file');
}
try {
$importedData = unserialize($fileData);
} catch (\Exception $e) {
throw new \Exception('Failed to deserialize data');
}
$campaignFields = [
'name' => 'required|string|max:100|unique:campaigns,name',
'template_id' => 'required',
'list_id' => 'required',
'subjectMode' => 'required|in:' . implode(',', SubjectMode::all()),
'subject' => 'required_if:subjectMode,' . SubjectMode::SINGLE,
'description' => 'nullable|string|max:2000',
'scheduled_at' => 'nullable|date|after:now'
];
$campaignValues = [];
$customValidationMessages = [];
foreach ($campaignFields as $field => $rules) {
$campaignValues[$field] = isset($importedData[$field]) ? trim($importedData[$field]) : null;
}
unset($field);
unset($rules);
//Handle template reference
if (!empty($importedData['template_name'])) {
$template = Template::where('name', $importedData['template_name'])->first();
$customValidationMessages['template_id.required'] = 'Template ' . $importedData['template_name'] . ' not found';
if (!empty($template)) {
$campaignValues['template_id'] = $template->id;
}
}
//Handle list reference
if (!empty($importedData['list_name'])) {
$list = RecipientList::where('name', $importedData['list_name'])->first();
$customValidationMessages['list_id.required'] = 'List ' . $importedData['list_name'] . ' not found';
if (!empty($list)) {
$campaignValues['list_id'] = $list->id;
}
}
//Handle group reference
if (!empty($importedData['group_name'])) {
$group = Group::where([['scope', '=', GroupScope::CAMPAIGNS], ['name', '=', $importedData['group_name']]])->first();
if (!empty($group)) {
$campaignValues['group_id'] = $group->id;
}
}
//Handle category reference
if (!empty($importedData['tracking_category_name'])) {
$category = TrackingCategory::where('name', $importedData['tracking_category_name'])->first();
if (!empty($category)) {
$campaignValues['tracking_category_id'] = $category->id;
}
}
//Check transport
if (!empty($importedData['transport_id'])) {
$campaignValues['transport_id'] = Config::has('mail.custom_smtp_transports.' . $importedData['transport_id']) ? $importedData['transport_id'] : EmailTransport::DEFAULT_TRANSPORT_ID;
}
/* @var $campaignValidator Validator */
$campaignValidator = Validator::make($campaignValues, $campaignFields, $customValidationMessages);
if ($campaignValidator->fails()) {
/* @var $errors MessageBag */
$errors = $campaignValidator->errors();
throw new \Exception('Campaign validation failed');
}
$campaign = new Campaign($campaignValues);
if ($campaign->save()) {
//Save subject(s)
if ($campaign->subjectMode == SubjectMode::SINGLE) {
//Subject already saved within campaign record
} elseif (SubjectMode::isDynamic($campaign->subjectMode)) {
$subjectOrder = 1;
foreach($importedData['dynamic_subjects'] as $item) {
$dSubject = new CampaignDynamicSubject(['campaign_id' => $campaign->id, 'value' => $item['value']]);
if (SubjectMode::SEQUENTIAL == $campaign->subjectMode) {
$nextSubjectOrder = ($subjectOrder === count($importedData['dynamic_subjects'])) ? 1 : ($subjectOrder + 1);
$dSubject->order = $subjectOrder;
$dSubject->next = $nextSubjectOrder;
$subjectOrder++;
}
$dSubject->save();
}
}
//Save headers
foreach($importedData['headers'] as $headerName => $headerValue) {
$header = new CampaignHeader(['campaign_id' => $campaign->id, 'name' => $headerName, 'value' => $headerValue]);
$header->save();
}
//Save variables
foreach($importedData['variables'] as $varName => $varValue) {
$variable = new CampaignVariable(['campaign_id' => $campaign->id, 'name' => $varName, 'value' => $varValue]);
$variable->save();
}
}
if ($request->get('importMode', 'import') == 'import_and_exit') {
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withMessage("Campaign #{$campaign->id} successfully imported !");
} elseif ($request->get('importMode', 'import') == 'import_and_edit') {
return Redirect::action('CampaignsController@edit', ['id' => $campaign->id])->withMessage("Campaign #{$campaign->id} just imported !");
} else {
return Redirect::action('CampaignsController@showImportForm')->withMessage("Campaign #{$campaign->id} successfully imported !");
}
} catch (\Exception $e) {
return Redirect::action('CampaignsController@showImportForm')->withError('Failed to import campaign. Reason: ' . $e->getMessage())->withErrors($errors)->withInput();
}
}
/**
* Show import gui
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showImportForm()
{
return view('campaigns.import');
}
/**
* Start sending campaign
*
* @param $id
* @return mixed
*/
public function run($id) {
try {
if (empty($id)) {
throw new \Exception('Empty id received');
}
$campaign = Campaign::find($id);
if (empty($campaign)) {
throw new \Exception('Campaign not found');
}
CampaignManager::run($campaign);
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withMessage("Campaign #$id will be launched in minute");
} catch (\Exception $e) {
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withError('Failed to run campaign. Reason: ' . $e->getMessage());
}
}
/**
* Stop sending campaign
*
* @param $id
* @return mixed
*/
public function pause($id) {
try {
if (empty($id)) {
throw new \Exception('Empty id received');
}
$campaign = Campaign::find($id);
if (empty($campaign)) {
throw new \Exception('Campaign not found');
}
CampaignManager::pause($campaign);
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withMessage("Campaign #$id paused");
} catch (\Exception $e) {
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withError('Failed to run campaign. Reason: ' . $e->getMessage());
}
}
/**
* Stop campaign & reset recipients states
*
* @param $id
* @return mixed
*/
public function stop($id) {
try {
if (empty($id)) {
throw new \Exception('Empty id received');
}
$campaign = Campaign::find($id);
if (empty($campaign)) {
throw new \Exception('Campaign not found');
}
CampaignManager::stop($campaign);
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withMessage("Campaign #$id stopped");
} catch (\Exception $e) {
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withError('Failed to run campaign. Reason: ' . $e->getMessage());
}
}
/**
* @param $id
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function downloadSkippedReport($id)
{
try {
if (empty($id)) {
throw new \Exception('Empty id received');
}
$campaign = Campaign::find($id);
if (empty($campaign)) {
throw new \Exception('Campaign not found');
}
return response()->download($campaign->getSkippedReportFilePath());
} catch (\Exception $e) {
return Redirect::route('campaigns.index', Session::get('campaigns.getParams'))->withError('Failed to download file. Reason: ' . $e->getMessage());
}
}
}
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\Config;
use PHPZen\LaravelRbac\Model\Role;
use Illuminate\Support\Facades\Log;
class AssignDefaultRole
{
/**
* Handle the event.
*
* @param Registered $event
* @return void
*/
public function handle(Registered $event)
{
$user = $event->user;
$default_roles = Config::get('rbac.default_roles');
if (!empty($default_roles) && is_array($default_roles)) {
foreach($default_roles as $slug) {
/* @var $r Role */
$r = Role::where('slug', $slug)->first();
$user->roles()->attach($r->id);
Log::info('Role assigned to user', ['uid' => $user->id, 'role' => $r->slug]);
}
} else {
Log::notice('Default user roles not defined');
}
}
}
<?php
namespace App\Jobs;
use App\Entities\EventAction;
use App\Exceptions\EventExpiredException;
use App\Exceptions\EventTriggerDisabledException;
use App\Exceptions\EventTriggerNotFoundException;
use App\Exceptions\MailArchiveException;
use App\Facades\BladeStringRenderer;
use App\Facades\DataContractsRepository;
use App\Facades\Mail;
use App\Models\Trigger;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\View;
use Swift_RfcComplianceException;
use Triton\DataContractProtocol\BaseEventContract;
use Triton\Entities\Recipient\RecipientField;
class ProcessEvent extends Job implements ShouldQueue {
use InteractsWithQueue, Queueable, SerializesModels;
/**
* Max attempts to process failed jobs
*/
const DEFAULT_JOB_PROCESS_ATTEMPTS = 3;
private $maxJobProcessAttempts = 0;
/**
* Create a new job instance.
*
*/
public function __construct()
{
view()->addLocation(Config::get('triton.data_dir') . 'templates' . DIRECTORY_SEPARATOR);
$this->maxJobProcessAttempts = Config::get('triton.event_process_attempts', self::DEFAULT_JOB_PROCESS_ATTEMPTS);
}
/**
* Execute the job.
*
* @return void
*/
public function fire($job, $data)
{
//Track job processing attempts
if (empty($data['attempts'])) {
$data['attempts'] = 1;
} else {
$data['attempts']++;
}
try {
$contract = DataContractsRepository::factory($data);
/* @var $contract BaseEventContract */
if (!$contract->isValid()) {
throw new \Exception('Data Contract validation failed');
}
$trigger = Trigger::findByEventId($contract->getEventId());
if (empty($trigger)) {
throw new EventTriggerNotFoundException('Trigger not found for event ' . $contract->getEventId());
}
if (!empty($contract->getTimestamp()) && !empty($trigger->event_ttl)) {
if (time() > ($contract->getTimestamp() + $trigger->event_ttl * 3600)) {
throw new EventExpiredException('Event expired');
}
}
if (!$trigger->is_active) {
throw new EventTriggerDisabledException('Trigger is disabled');
}
$this->pull($trigger, $contract);
} catch (MailArchiveException $mae) {
Log::warning('Event processed, but mail is not archived', ['reason' => $mae->getMessage()]);
} catch (\Exception $e) {
//Event should be skipped
if (($e instanceof EventTriggerNotFoundException) ||
($e instanceof EventTriggerDisabledException) ||
($e instanceof EventExpiredException) ||
($e instanceof Swift_RfcComplianceException)) {
Cache::tags(['today_counters'])->increment('event.skipped');
Log::info('Event skipped', ['reason' => $e->getMessage()]);
} else {
//Event should be precessed again
if ($data['attempts'] < $this->maxJobProcessAttempts) {
Queue::pushOn(Config::get('queue.failed_events_queue_name'), self::class, $data);
Cache::tags(['today_counters'])->increment('event.moved_to_failed_queue');
Log::error('Failed to process event. Moved to failed jobs queue', ['reason' => $e->getMessage(), 'attempts' => $data['attempts']]);
} else {
Log::error('Failed to process event. Max process attempts exceed. Event skipped', ['reason' => $e->getMessage(), 'attempts' => $data['attempts']]);
}
}
} finally {
$job->delete();
}
}
/**
* Execute trigger scenario
*
* @param Trigger $trigger
* @param BaseEventContract $contract
* @throws \Exception
*/
private function pull(Trigger $trigger, BaseEventContract $contract)
{
Cache::tags(['today_counters'])->increment("trigger.{$trigger->id}.pulled");
$variables = $trigger->getActualContext($contract);
$userId = empty($variables[RecipientField::USER_ID]) ? null : $variables[RecipientField::USER_ID];
$userCid = empty($variables[RecipientField::CID]) ? null : $variables[RecipientField::CID];
$userEmail = empty($variables[RecipientField::EMAIL]) ? null : $variables[RecipientField::EMAIL];
View::share($variables);
try {
$subject = BladeStringRenderer::render($trigger->resolveSubject($userId));
} catch (\Exception $e) {
throw new \Exception("Failed to render subject (trigger id #{$trigger->id}). Reason: " . $e->getMessage());
}
try {
$renderAttempts = Config::get('triton.template_render_attempts', 3);
do {
$body = view($trigger->template->getViewName())->render();
$renderAttempts --;
if (empty($body)) {
usleep(500000);
}
} while (($renderAttempts > 0) && empty($body));
if (empty($body)) {
throw new \Exception('Empty template rendered');
}
} catch (\Exception $e) {
throw new \Exception('Failed to render template. Reason: ' . strtolower($e->getMessage()));
}
Mail::send($subject, $body, $userEmail, $trigger->headersToArray(), $trigger->transport_id, Config::get('triton.archive_trigger_mails'));
if ((count(Mail::failures()) == 0) && $trigger->trackingEnabled()) {
dispatch(
(new EnqueueStatEvent(
$trigger->trackingCategory->id,
$trigger->trackingCategory->name,
EventAction::SEND,
$userId,
$userCid)
)->onQueue(Config::get('queue.stats_queue_name')));
}
}
}
<?php
namespace App\Models;
use App\Entities\CampaignCounter;
use App\Entities\CampaignState;
use App\Entities\RecipientState;
use App\Entities\SubjectMode;
use App\Models\Traits\RecipientTestContext;
use App\Models\Traits\SubjectResolver;
use App\Utils\Campaign\Manager;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Storage;
use Triton\Entities\Recipient\RecipientField;
use Triton\Entities\Variable\Context;
use Triton\Entities\Variable\VariableField;
class Campaign extends Model {
use SubjectResolver, RecipientTestContext;
protected $table = 'campaigns';
public $timestamps = false;
public $fillable = [
'name',
'template_id',
'list_id',
'subjectMode',
'subject',
'description',
'tracking_category_id',
'group_id',
'transport_id',
'scheduled_at'
];
/**
* Relationship with template
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function template()
{
return $this->hasOne(Template::class, 'id', 'template_id');
}
/**
* Relationship with list
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function recipientsList()
{
return $this->hasOne(RecipientList::class, 'id', 'list_id');
}
/**
* Relationship with tracking category
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function trackingCategory()
{
return $this->hasOne(TrackingCategory::class, 'id', 'tracking_category_id');
}
/**
* Relationship with group
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function group()
{
return $this->hasOne(Group::class, 'id', 'group_id');
}
/**
* Returns related variables
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public function variables()
{
return $this->hasMany(CampaignVariable::class)->get();
}
/**
* Returns user defined variables map
*
* @return array
*/
public function varsToArray()
{
return $this->variables()->pluck('value', 'name')->toArray();
}
/**
* Returns related headers
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public function headers()
{
return $this->hasMany(CampaignHeader::class)->get();
}
/**
* Returns header map
*
* @return array
*/
public function headersToArray()
{
return $this->headers()->pluck('value', 'name')->toArray();
}
/**
* Delete all headers
*/
public function deleteAllHeaders()
{
return CampaignHeader::where('campaign_id', $this->id)->delete();
}
/**
* Delete all variables
*/
public function deleteAllVariables()
{
return CampaignVariable::where('campaign_id', $this->id)->delete();
}
/**
* Returns related dynamic subjects
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function dynamicSubjects()
{
return $this->hasMany(CampaignDynamicSubject::class);
}
/**
* Delete all dynamic subjects
*/
public function deleteAllDynamicSubjects()
{
return CampaignDynamicSubject::where('campaign_id', $this->id)->delete();
}
/**
* @param array $options
* @return bool
*/
public function save(array $options = [])
{
if (SubjectMode::isDynamic($this->subjectMode)) {
$this->subject = null;
}
return parent::save($options);
}
/**
* Returns exportable presentation of campaign
*
* @return array
*/
public function getExportableData()
{
return [
'name' => $this->name,
'subjectMode' => $this->subjectMode,
'subject' => $this->subject,
'description' => $this->description,
'template_name' => empty($this->template_id) ? null : $this->template->name,
'list_name' => empty($this->list_id) ? null : $this->recipientsList->name,
'transport_id' => empty($this->transport_id) ? EmailTransport::DEFAULT_TRANSPORT_ID : $this->transport_id,
'tracking_category_name' => empty($this->tracking_category_id) ? null : $this->trackingCategory->name,
'group_name' => empty($this->group_id) ? null : $this->group->name,
'dynamic_subjects' => ($this->subjectMode !== SubjectMode::SINGLE) ? $this->dynamicSubjects()->get()->toArray() : [],
'headers' => $this->headersToArray(),
'variables' => $this->varsToArray(),
'scheduled_at' => $this->scheduled_at
];
}
/**
* Returns array of campaign contexts
*
* @return array
*/
public static function getContextOptions()
{
return self::orderBy('name')->pluck('name', 'id')->mapWithKeys(function($item, $key) {return ['campaign_' . $key => $item];})->toArray();
}
/**
* Returns test-mode context (variables with meta)
*
* @return array
*/
public function getTestContext()
{
$campaignVars = [];
$campaignVars = $this->variables()->pluck('name', 'value')->mapWithKeys(
function($varName, $varValue) {
return [$varName => [
VariableField::CONTEXT => Context::CAMPAIGN,
VariableField::DESCRIPTION => 'Custom campaign variable',
VariableField::TEST_VALUE => $varValue
]
];
})->toArray();
$listVars = [];
$listColumns = $this->recipientsList->getDataTableColumns();
//Some columns are not available in campaign context
$listColumns = array_diff($listColumns, RecipientField::all());
foreach ($listColumns as $column) {
$listVars[$column] = [
VariableField::CONTEXT => Context::RECIPIENT_LIST,
VariableField::DESCRIPTION => "List '{$this->recipientsList->name}' column value",
VariableField::TEST_VALUE => DB::table($this->recipientsList->getDataTableName())
->select($column)
->whereNotNull($column)
->limit(1)
->value($column)
];
}
$runtimeVars = [];
$runtimeVars['tracking_category_id'] = [
VariableField::CONTEXT => Context::CAMPAIGN,
VariableField::TEST_VALUE => $this->trackingEnabled() ? $this->tracking_category_id : 0,
VariableField::DESCRIPTION => 'Campaign tracking category ID'
];
$runtimeVars['tracking_category_name'] = [
VariableField::CONTEXT => Context::CAMPAIGN,
VariableField::TEST_VALUE => $this->trackingEnabled() ? $this->trackingCategory->name : '',
VariableField::DESCRIPTION => 'Campaign tracking category name'
];
return array_merge($this->getRecipientTestContext(), $campaignVars, $listVars, $runtimeVars);
}
/**
* Returns actual context
*
* @param array $recipientData
* @return array
*/
public function getActualContext(array $recipientData)
{
$campaignVars = [];
$campaignVars = $this->varsToArray();
$runtimeVars = [];
if ($this->trackingEnabled()) {
$runtimeVars['tracking_category_id'] = $this->tracking_category_id;
$runtimeVars['tracking_category_name'] = $this->trackingCategory->name;
}
return array_merge($recipientData, $campaignVars, $runtimeVars);
}
/**
* Set campaign set
*
* @param $state
* @param string $stateVerbose
* @return $this
*/
public function setState($state, $stateVerbose = '')
{
$oldState = $this->getState();
$this->state = $state;
$this->state_verbose = $stateVerbose;
if ($this->update()) {
Cache::forever("campaign:{$this->id}_state", $state);
Log::info('Campaign state changed', ['campaign' => $this->id, 'state' => "$oldState ==> $state", 'verbose' => $stateVerbose]);
}
return $this;
}
/**
* Returns campaign state
*
* @return mixed
*/
public function getState()
{
return Cache::rememberForever("campaign:{$this->id}_state", function () {
return $this->state;
});
}
/**
* Returns true if campaign in one of idle states
*
* @return bool
*/
public function isIdle()
{
return in_array($this->getState(), CampaignState::getIdleStates());
}
/**
* Returns recipients count
*
* @return mixed
*/
public function getListSize()
{
return $this->recipientsList->getRecipientsCount();
}
/**
* Returns true if campaign running
*
* @return bool
*/
public function isRunning()
{
return $this->getState() == CampaignState::RUNNING;
}
/**
* Delete campaign
*
* @return bool|null
* @throws \Exception
*/
public function delete()
{
Manager::deleteCampaignAllCounters($this);
Manager::forgetRecipientStates($this);
$this->deleteSkippedReport();
return parent::delete();
}
/**
* Returns true if campaign has related tracking category
*
* @return bool
*/
public function trackingEnabled()
{
return !empty($this->trackingCategory);
}
/**
* Returns default subject
* used as fallback
*
* @return mixed
*/
protected function getDefaultSubject()
{
return Config::get('triton.default_campaign_subject');
}
/**
* Should campaign start now?
*
*/
public function scheduledNow()
{
return Carbon::parse($this->scheduled_at)->isSameAs('Y-m-d H:i', Carbon::now());
}
/**
* Do we processed whole campaign?
*/
public function isSentOut()
{
$processedCount = Manager::getCampaignCounter($this, CampaignCounter::PROCESSED);
$enqueuedCount = Manager::getCampaignCounter($this, RecipientState::ENQUEUED);
return (!empty($processedCount) && !empty($enqueuedCount) && ($processedCount == $enqueuedCount));
}
/**
* Returns short file name
*
* @return string
*/
public function getSkippedReportFileName()
{
return "campaign_{$this->id}_report.txt";
}
/**
* Returns absolute file path
*
* @return string
*/
public function getSkippedReportFilePath()
{
return Storage::disk('reports')->getDriver()->getAdapter()->getPathPrefix() . $this->getSkippedReportFileName();
}
/**
* Returns true if report file exists
*
* @return mixed
*/
public function hasSkippedReport()
{
return Storage::disk('reports')->exists($this->getSkippedReportFileName());
}
/**
* Delete skipped recipients report
*/
public function deleteSkippedReport()
{
if ($this->hasSkippedReport()) {
Storage::disk('reports')->delete($this->getSkippedReportFileName());
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment