Mailchimp API - batch upload - php

I'm trying to import a list of 879 users (in batch) into a Mailchimp list. The library I'm using is: https://github.com/pacely/mailchimp-api-v3.
I've created a Laravel console command to do this. The code is:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Mailchimp\Mailchimp;
use Exception;
class ImportContactsIntoMailchimp extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'mailchimp:import:contacts';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Import contacts into mailchimp.';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* If an e-mail address starts with this, mailchimp won't allow it
* http://kb.mailchimp.com/lists/growth/limits-on-role-based-addresses
*
* #var array
*/
private $nomail = array(
'abuse#',
'admin#',
'billing#',
'compliance#',
'devnull#',
'dns#',
'ftp#',
'hostmaster#',
'inoc#',
'ispfeedback#',
'ispsupport#',
'list-request#',
'list#',
'maildaemon#',
'noc#',
'no-reply#',
'noreply#',
'null#',
'phish#',
'phishing#',
'postmaster#',
'privacy#',
'registrar#',
'root#',
'security#',
'spam#',
'support#',
'sysadmin#',
'tech#',
'undisclosed-recipients#',
'unsubscribe#',
'usenet#',
'uucp#',
'webmaster#',
'www#'
);
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
$result = DB::table('contacts_billonline')
->select(
'email',
'firstname',
'surname',
'phone'
)
->get();
$batch_count = 250;
$email_column = 'email';
$merge_columns = array(
'EMAIL' => 'email',
'FNAME' => 'firstname',
'LNAME' => 'surname',
'PHONE' => 'phone',
);
$listId = $this->ask('What is the list id?');
// Create our mailchimp connection
$mailchimp_key = config('mailchimp.apikey');
$mc = new Mailchimp($mailchimp_key);
$contact_list = '/lists/'.$listId.'/members';
$total = count($result);
for ($i = 0; $i < $total; $i += $batch_count)
{
$batch = array();
for ($j = 0; $j < $batch_count; $j++)
{
$row_number = $i + $j;
if ($row_number === $total)
{
// If we reached the end of the list, stop trying to add operations
break;
}
// Get the email address from the result for this row
$email = &$result[$row_number]->{$email_column};
// Is this a valid email address? If so add it to the batch
if ($this->isValidEmail($email)) {
$this->info($email);
// Get our merge columns for this row and put them in the array
$merge_fields = array();
foreach ($merge_columns as $key => &$column) {
$merge_fields[$key] = &$result[$row_number]->{$column};
}
$insert_email = array(
'email_address' => $email,
'status' => 'subscribed',
'merge_fields' => $merge_fields,
'tags' => ['Contacts']
);
$batch_operation = array(
'method' => 'POST',
'path' => $contact_list,
'body' => json_encode($insert_email)
);
$batch[] = $batch_operation;
}
}
$body = array();
$body['operations'] = $batch;
$batch_result = $mc->post('/batches', $body);
$this->info($batch_result);
}
}
/**
* Is this is an email address mailchimp would see as valid?
*
* #param $email
* #return bool
*/
function IsValidEmail(&$email)
{
foreach ($this->nomail as &$bad_mail)
{
if (strpos($email, $bad_mail) === 0)
{
return false;
}
}
$validator = Validator::make(
array(
'email' => &$email
),
array(
'email' => 'required|email'
)
);
if ($validator->fails())
{
return false;
}
return true;
}
}
I'm trying to import the users from a database table in batch into Mailchimp.
The API key is set correctly. I'm also trying to set a tag "Contacts" to the subscribers.
The list id is also set correctly.
The problem is that the list isn't imported. The result in terminal is:
{"id":"c48cd17f6d","status":"pending","total_operations":0,"finished_operations":0,"errored_operations":0,"submitted_at":"2019-07-02T10:58:22+00:00","completed_at":"","response_body_url":"","_links":[{"rel":"parent","href":"https://us8.api.mailchimp.com/3.0/batches","method":"GET","targetSchema":"https://us8.api.mailchimp.com/schema/3.0/Definitions/Batches/CollectionResponse.json","schema":"https://us8.api.mailchimp.com/schema/3.0/CollectionLinks/Batches.json"},{"rel":"self","href":"https://us8.api.mailchimp.com/3.0/batches/c48cd17f6d","method":"GET","targetSchema":"https://us8.api.mailchimp.com/schema/3.0/Definitions/Batches/Response.json"},{"rel":"delete","href":"https://us8.api.mailchimp.com/3.0/batches/c48cd17f6d","method":"DELETE"}]}
What am I doing wrong?

Related

REST API - Different Behaviour on Different Servers

We have created a REST API PHP setup which is designed to compare the Tables of one database with another and if the user doesn't exist in one, create it in the Application. Also if there is a change to a user i.e. Email address, string etc then update it.
We've had a script created which using php works fantastically on our test server, it creates a user when required and updates them when required.
When we move this over to a different server which has the same database structure, same tables, same root username and password etc, it isn't recognising the existing users. It thinks it has xx,xxx users to create and then fails as 'user with that username already exists'.
Does any one have any ideas why this maybe the case? Below is the main php file which then has a config file, a db.php, a common.php and then a GpsGateapi.php
require_once('rest_api_includes/Config.php');
require_once(Config::API_INCLUDES_DIR . '/DB.php');
require_once(Config::API_INCLUDES_DIR . '/Gpsgate_API.php');
require_once(Config::API_INCLUDES_DIR . '/Common.php');
class API_Sync
{
/** #var int application id */
private $application_id;
/** #var int user type id */
private $user_type_id;
/** #var Gpsgate_API instance */
private $api;
/** #var DB instance */
private $db;
/** #var string<show|log> Show or log errors */
private $errors_report_type;
/** #var string<show|log|none> Show or log actions */
private $actions_report_type;
/**
* Sets variables, connects to database and REST API
*
* #throws \Exception
*/
public function __construct()
{
$this->application_id = Config::API_APPLIATION_ID;
$this->user_type_id = Config::API_USER_TYPE_ID;
$this->errors_report_type = Config::API_SHOW_OUTPUT ? 'show' : 'log';
$this->actions_report_type = Config::API_SHOW_OUTPUT ? 'show' : (Config::API_ACTIONS_LOGGING ? 'log' : 'none');
}
/**
* Main method that controlls all the process
*
* #throws \Exception
*/
public function run()
{
$this->prepare();
$gpsgate_users_result = $this->api->getUsers();
if ($gpsgate_users_result->status !== 200) {
throw new Exception(implode('; ', $gpsgate_users_result->body));
}
$gpsgate_users = $gpsgate_users_result->body;
$db_users = $this->db->get(DB::TBL_PERSONS);
$res = $this->compare($gpsgate_users, $db_users);
$this->updateUsers($res['update']);
$this->createUsers($res['create']);
}
/**
* Create gsgate users data from database array
*
* #param array $users
*/
private function createUsers($users)
{
if (!empty($users)) {
$this->logOrShowAction("Begin to create users data.");
foreach ($users as $user) {
$res = $this->api->createUser([
'email' => $user['Email'],
'name' => $user['FirstName'],
'surname' => $user['LastName'],
'driverID' => $user['RFID'],
'username' => $user['UserName'],
'password' => Common::generatePassword(),
'userTypeId' => $this->user_type_id,
'description' => $user['Location'],
]);
if ($res->status !== 200) {
throw new Exception($res->body);
}
}
$this->logOrShowAction("Done.");
}
}
/**
* Update gsgate users data from database array
*
* #param array $users
*/
private function updateUsers($users)
{
if (!empty($users)) {
$this->logOrShowAction("Begin to update users data.");
foreach ($users as $user) {
$res = $this->api->updateUser([
'email' => $user['Email'],
'name' => $user['FirstName'],
'surname' => $user['LastName'],
'driverID' => $user['RFID'],
'description' => $user['Location'],
], $user['gpsgate_id']);
if ($res->status !== 200) {
throw new Exception($res->body);
}
}
$this->logOrShowAction("Done.");
}
}
/**
* Compare arrays and return list of users data to update/create
*/
private function compare($gpsgate_users, $db_users)
{
$this->logOrShowAction("Begin to compare users data.");
$gpsgate_user_key = 'username';
$db_user_key = 'UserName';
$gpsgate_users = Common::setIndexesByKey($gpsgate_user_key, $gpsgate_users);
$res = [
'update' => [],
'create' => [],
];
foreach ($db_users as $user) {
$user_key = $user[$db_user_key];
if (!empty($gpsgate_users[$user_key])) {
if ($this->userInfoVary($gpsgate_users[$user_key], $user)) {
$user['gpsgate_id'] = $gpsgate_users[$user_key]->id; // add gpsgate id
$res['update'][] = $user;
}
} else {
$res['create'][] = $user;
}
}
$this->logOrShowAction('Done');
$this->logOrShowAction('Need to create: ' . count($res['create']) . ' rows; to update: ' . count($res['update']) . ' rows.');
return $res;
}
/**
* Check is the information between db user and api user is different
*/
private function userInfoVary($gpsgate_user, $db_user)
{
return $db_user['Email'] != $gpsgate_user->email
|| $db_user['FirstName'] != $gpsgate_user->name
|| $db_user['LastName'] != $gpsgate_user->surname
|| $db_user['RFID'] != ($gpsgate_user->driverID ?? '')
|| $db_user['Location'] != $gpsgate_user->description;
}
/**
* Connects to database, REST API, creates folders for errors and actions if need
*/
private function prepare()
{
if ($this->errors_report_type == 'log') {
if (!file_exists(dirname(Config::API_LOG_FILE_ERRORS))) {
mkdir(dirname(Config::API_LOG_FILE_ERRORS));
}
if (!file_exists(Config::API_LOG_FILE_ERRORS)) {
file_put_contents(Config::API_LOG_FILE_ERRORS, '');
}
}
if ($this->actions_report_type == 'log') {
if (!file_exists(dirname(Config::API_LOG_FILE_ACTIONS))) {
mkdir(dirname(Config::API_LOG_FILE_ACTIONS));
}
if (!file_exists(Config::API_LOG_FILE_ACTIONS)) {
file_put_contents(Config::API_LOG_FILE_ACTIONS, '');
}
}
$this->logOrShowAction('Trying to connect to database and GPSGATE REST API.');
$this->api = new Gpsgate_API(Config::API_APPLIATION_ID, Config::API_USER_TYPE_ID);
$this->db = DB::instance();
$this->logOrShowAction('Done.');
}
/**
* Logs error message in file or output in browser
*
* #param string $msg
*/
public function logOrShowError($msg)
{
$msg = "<span style='color: red; font-weight: 600;'>Error: " . $msg . "</span>";
$this->writeOrEchoMessage($msg, Config::API_LOG_FILE_ERRORS, $this->errors_report_type);
}
/**
* Logs action message in file or output in browser
*
* #param string $msg
*/
public function logOrShowAction($msg)
{
$this->writeOrEchoMessage($msg, Config::API_LOG_FILE_ACTIONS, $this->actions_report_type);
}
private function writeOrEchoMessage($msg, $file, $report_type)
{
if ($report_type == 'none') {
return ;
}
$msg = '[' . date('Y-m-d H:i:s') . '] ' . $msg;
if ($report_type == 'show') {
echo $msg . '<br>';
} else {
$h = fopen($file, 'a+');
fwrite($h, strip_tags($msg) . PHP_EOL);
fclose($h);
}
}
}
$sync = new API_Sync();
try {
$sync->run();
} catch (\Exception $e) {
$sync->logOrShowError($e->getMessage());
}

Laravel 5.6, where to keep additional classes (additional functional)?

I need to make request on some CRM api in my controller. For making this I have pretty big method. It's look like ugly. I know that there are some "Services" and to put additional code into Service is a good way. But I don't know what is this. Is it a custom classes into app folder? Or maybe it's Service-providers? I have read service-providers documentation and I'm not sure that service-providers is suitable for this. Here is my code:
<?php
namespace App\Http\Controllers;
use App\User;
use App\UserInfo;
use Validator;
use Illuminate\Http\Request;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$users = User::with('info')
->paginate(20);
$users->withPath(DIRECTORY_SEPARATOR . $request->path() .DIRECTORY_SEPARATOR);
return response()->json($users)->setEncodingOptions(JSON_UNESCAPED_UNICODE);
}
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$data = $request->json()->all();
$rules = [
'name' => 'required',
'phone' => 'required|unique:users'
];
$validator = Validator::make($data, $rules);
if ($validator->fails()) return response()->json(['errors'=>$validator->errors()]);
$user = new User();
$user->name = request('name');
$user->phone = request('phone');
$user_info_obj = $this->storeUserInfo();
if($user_info_obj === null){
return response('Impassible to define user geo data', 400);
}
$user->info_id = $user_info_obj->id;
$user->save();
$this->makeAMOLead($user->name,
$user->phone,
$user_info_obj->user_agent,
$user_info_obj->city,
$user_info_obj->country);
return response()->json(['success' => 'User created successfully']);
}
public function storeUserInfo()
{
$ip = request()->ip();
$reader = new \GeoIp2\Database\Reader('../resources/geo-lite2-city_20180807/GeoLite2-City.mmdb');
try {
$record = $reader->city($ip);
}
catch (\Throwable $e){
// Code bellow is for testing on localhost, Because of maybe exception instead of geo obj on localhost.
$info = new UserInfo();
$info->ip = '127.0.0.1';
$info->city = 'Some city';
$info->country = 'Some country';
$info->country_code = 'Some code';
$info->continent = 'Some continent';
$info->continent_code = 'no';
$info->user_agent = 'User agent';
$info->save();
return $info;
//return null;
}
$city = $record->city->names['ru'];
$continent = $record->continent->names['ru'];
$continent_code = $record->continent->code;
$country = $record->country->names['ru'];
$country_code = $record->country->isoCode;
$user_agent = \request()->userAgent();
$info = new UserInfo();
$info->ip = $ip;
$info->city = $city;
$info->country = $country;
$info->country_code = $country_code;
$info->continent = $continent;
$info->continent_code = $continent_code;
$info->user_agent = $user_agent;
$info->save();
return $info;
}
private function makeAMOLead($name, $phone, $userAgent, $city, $country)
{
$domain = env('AMO_DOMAIN');
$login = env('AMO_LOGIN');
$hash = env('AMO_HASH');
try {
$credentials = new \ddlzz\AmoAPI\CredentialsManager($domain, $login, $hash);
$settings = new \ddlzz\AmoAPI\SettingsStorage();
$settings->setCookiePath(env('AMO_COOKIE_FILE_PATH'));
$request = \ddlzz\AmoAPI\ClientFactory::create($credentials, $settings);
$lead = new \ddlzz\AmoAPI\Model\Amo\Lead();
$lead['name'] = $name;
if(env('AMO_PIPELINE_ID', null)){
$lead['pipeline_id'] = intval(env('AMO_PIPELINE_ID'));
}
$lead['name'] = 'New pickup user ' . $name;
$lead['custom_fields'] = [
[
'id' => env('AMO_NAME_FIELD_ID'),
'values' => [
['value' => $name],
]
],
[
'id' => env('AMO_USER_AGENT_FIELD_ID'),
'values' => [
['value' => $userAgent]
]
],
[
'id' => env('AMO_CITY_FIELD_ID'),
'values' => [
['value' => $city]
]
],
[
'id' => env('AMO_COUNTRY_FIELD_ID'),
'values' => [
['value' => $country]
]
],
];
$lead['created_at'] = time();
$result = $request->add($lead);
$pipelineId = json_decode($result)->_embedded->items{0}->id;
// create contact
$contact = new \ddlzz\AmoAPI\Model\Amo\Contact();
$contact['name'] = $name;
$contact['created_at'] = time();
$contact['leads_id'] = "$pipelineId";
// dd($request->accountInfo(), true); // Call this, if you need to know ids of default fields (like phone, or position)
$contact['custom_fields'] = [
[
'id' => env('AMO_CONTACT_PHONE_ID'),
'values' => [
[
'value' => $phone,
'enum' => 'MOB',
],
]
],
];
$result = $request->add($contact);
} catch (Exception $e) {
echo response()->json(['error' => $e->getFile() . ': ' . $e->getMessage()]);
}
}
}
Look on the makeAMOLead. This is big method in my controller and this is not ok for controller conception.
Please use repository pattern to split all the communication between the application and your data source. and call the repository functions inside your controller. It is good practice. Here is an article you can understand about that
Example:
Your functions can be separate from controller to repository.
storeUserInfo
makeAMOLeadin
Move your functions an repository and call them into your controller.

How to collect tweets/retweets using GNIP API in php

Since twitter has switched to streaming his API, we have to collect the data by ourselves.
How we can do it using GNIP API in php?
Answering this, I’ve just wanted to ensure that I’ve done everything right and maybe to improve my TwitterGnipClient class with your help.
But if there are no answers, let it be FAQ style question.
Main methods see below:
/**
* Add rules to PowerTrack stream’s ruleset.
* #example ['id' => 'url', ...]
* #param array $data
*/
public function addRules(array $data)
{
$rules = [];
foreach ($data as $id => $url) {
$rules[] = $this->buildRuleForUrl($url, $id);
}
$this->httpClient->post($this->getRulesUrl(), [
'auth' => [$this->getUser(), $this->getPassword()],
'json' => ['rules' => $rules]
]);
}
/**
* Retrieves all existing rules for a stream.
* #return \Generator
*/
public function getRules()
{
$response = $this->httpClient->get($this->getRulesUrl(), [
'auth' => [$this->getUser(), $this->getPassword()]
]);
$batchStr = '';
$body = $response->getBody();
while (!$body->eof()) {
$batchStr .= $body->read(1024);
}
$batchArray = explode(PHP_EOL, $batchStr);
unset($batchStr);
foreach ($batchArray as $itemJson) {
yield $this->unpackJson($itemJson);
}
}
/**
* Removes the specified rules from the stream.
* #param $data
*/
public function deleteRules($data)
{
$rules = [];
foreach ($data as $id => $url) {
$rules[] = $this->buildRuleForUrl($url, $id);
}
$this->httpClient->delete($this->getRulesUrl(), [
'auth' => [$this->getUser(), $this->getPassword()],
'json' => ['rules' => array_values($rules)]
]);
}
/**
* Open stream through which the social data will be delivered.
* #return \Generator
*/
public function listenStream()
{
$response = $this->httpClient->get($this->getStreamUrl(), [
'auth' => [$this->getUser(), $this->getPassword()],
'stream' => true
]);
$batchStr = '';
$body = $response->getBody();
while (!$body->eof()) {
$batchStr .= $body->read(1024);
$batchArray = explode(PHP_EOL, $batchStr);
// leave the last piece of response as it can be incomplete
$batchStr = array_pop($batchArray);
foreach ($batchArray as $itemJson) {
yield $this->processBroadcastItem($this->unpackJson($itemJson));
}
}
$body->close();
}
/**
* Process broadcast item data
* #param $data
* #return array
*/
protected function processBroadcastItem($data)
{
if (is_array($data)) {
$url = str_replace('url_contains:', '', $data['gnip']['matching_rules'][0]['value']);
switch ($data['verb']) {
// Occurs when a user posts a new Tweet.
case 'post':
return $this->getMappedResponse($url, 'tweet', 1);
break;
// Occurs when a user Retweets another user's Tweet
case 'share':
return $this->getMappedResponse($url, 'retweet', $data['retweetCount']);
break;
}
}
return [];
}
All class I shared as Gist - here
P.S. If you see an evident issue - comment it please.

A form can only be submitted once 500 Internal Server Error - AlreadySubmittedException

I am trying to create a login/registration form using FOSUserBundle. After logging in the user gets a homepage. wherein he has to choose two different events for 2 time slots from two radio button type options and hit submit. Also if a user has registered already and logs in, then he can see his previously selected choices. Also he can change them. When I created the homepage from inside the controller, the code worked fine.
Here is the controller code:
<?php
namespace AppBundle\Controller;
use AppBundle\Entity\events;
//use AppBundle\Entity\eventtype;
use AppBundle\Entity\users;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class DefaultController extends Controller {
/**
* #Route("/home", name="homepage")
*/
public function indexAction(Request $request) {
$events = new events();
$greet = 'Welcome to the Birthday Party registration!';
$selection1 = '';
$selection2 = '';
$et1 = 0;
$et2 = 0;
//get the events repository
$repository = $this->getDoctrine()->getRepository('AppBundle:events');
//get the user_id of the logged in user
$user = $this->container->get('security.context')->getToken()->getUser();
$events->setUser($user);
$x = $events->getUser()->getID();
//check if the user has already registered or not
$y = $repository->findOneBy(array('user' => $x));
//If the user has registered already, set the data value for the form
if($y){
$et1 = $y->getET1();
$et2 = $y->getET2();
}
//create form
$form = $this->createFormBuilder($events)
->add('eT1', ChoiceType::class, array(
'choices' => array(
'Poker' => 1,
'Chess' => 2,
'Cricket' => 3,
'Marbles' => 4,
'Football' => 5,
),
'choices_as_values' => true,
'expanded' => true,
'multiple' => false,
'label' => 'Choose After Breakfast Event',
'data' => $et1
))
->add('eT2', ChoiceType::class, array(
'choices' => array(
'Poker' => 1,
'Chess' => 2,
'Cricket' => 3,
'Marbles' => 4,
'Football' => 5,
),
'choices_as_values' => true,
'expanded' => true,
'multiple' => false,
'label' => 'Choose After Snacks Event',
'data' => $et2
))
->add('save', SubmitType::class, array('label' => 'Submit'))
->getForm();
//retrieve the choices array for eT1 and eT2
$eT1Choices = $form->get('eT1')->getConfig()->getOption('choices');
$eT2Choices = $form->get('eT2')->getConfig()->getOption('choices');
//intialize the eventname variables
$eT1Name = '';
$eT2Name = '';
if ($y) {
//If the user has registered already, display his previously selected options
$selection1 = 'Your After Breakfast event:';
$selection2 = 'Your After Snacks event:';
//set the eventname based on the value of et1
foreach ($eT1Choices as $key => $value) {
if ($et1 == $value) {
$eT1Name = $key;
}
}
//set the eventname based on the value of et2
foreach ($eT2Choices as $key => $value) {
if ($et2 == $value) {
$eT2Name = $key;
}
}
}
//after submission
if ($request->isMethod('POST')) {
$form->submit($request);
//retrieve maxlimit parameters from parameters.yml
$maxPoker = $this->container->getParameter('pokermaxlimit');
$maxChess = $this->container->getParameter('chessmaxlimit');
$maxCricket = $this->container->getParameter('cricketmaxlimit');
$maxMarbles = $this->container->getParameter('marblesmaxlimit');
$maxFootball = $this->container->getParameter('footballmaxlimit');
//initialize $eventMaxLim
$eventMaxLim = 0;
//retrieve form data
$formData = $form->getData();
$ET1 = $formData->getET1();
$ET2 = $formData->getET2();
$selection1 = 'Your After Breakfast event:';
$selection2 = 'Your After Snacks event:';
//set the eventname based on the value of eT1
foreach ($eT1Choices as $key => $value) {
if ($ET1 == $value) {
$eT1Name = $key;
}
}
//set the eventname based on the value of eT2
foreach ($eT2Choices as $key => $value) {
if ($ET2 == $value) {
$eT2Name = $key;
}
}
//check to see if the user has registered the same event for eT1 and eT2
if ($ET1 == $ET2) {
$this->get('session')->getFlashBag()->set('error', 'You have chosen same events for both time slots! Please choose different ones.');
}
//check to see how many users have registered for the opted event(eT1)
$query1 = $repository->createQueryBuilder('p')
->select('count(p)')
->where('p.eT1 = :eT1')
->setParameter('eT1', $ET1)
->getQuery();
$a = $query1->getSingleScalarResult();
//set the $eventMaxLim based on the chosen event for eT1
if ($ET1 == 1) {
$eventMaxLim = $maxPoker;
} else if ($ET1 == 2) {
$eventMaxLim = $maxChess;
} else if ($ET1 == 3) {
$eventMaxLim = $maxCricket;
} else if ($ET1 == 4) {
$eventMaxLim = $maxMarbles;
} else if ($ET1 == 5) {
$eventMaxLim = $maxFootball;
}
//check to see if the after breakfast event (eT1) is full (ie.has reached the maxlimit)
if ($a >= $eventMaxLim) {
$this->get('session')->getFlashBag()->set('error', 'choose another After Breakfast event, this one is full');
}
//check to see how many users have registered for the opted event(eT2)
$query2 = $repository->createQueryBuilder('p')
->select('count(p)')
->where('p.eT2 = :eT2')
->setParameter('eT2', $ET2)
->getQuery();
$b = $query2->getSingleScalarResult();
//set the $eventMaxLim based on the chosen event for eT2
if ($ET2 == 1) {
$eventMaxLim = $maxPoker;
} else if ($ET2 == 2) {
$eventMaxLim = $maxChess;
} else if ($ET2 == 3) {
$eventMaxLim = $maxCricket;
} else if ($ET2 == 4) {
$eventMaxLim = $maxMarbles;
} else if ($ET2 == 5) {
$eventMaxLim = $maxFootball;
}
//check to see if the after snacks event (eT2) is full (ie.has reached the maxlimit)
if ($b >= $eventMaxLim) {
$this->get('session')->getFlashBag()->set('error', 'choose another After Snacks event, this one is full');
}
if (($a < $eventMaxLim) && ($b < $eventMaxLim) && ($ET1 != $ET2)) {
if ($form->isValid()) {
//get the entity manager
$em = $this->getDoctrine()->getManager();
// If the user is registering for the first time (execute the Insert query)
if (!$y) {
$em->persist($events);
$em->flush();
//return $this->redirectToRoute('homepage');
}
//If the user has registered already and want change his registered events (execute the Update query)
else {
$y->setET1($ET1);
$y->setET2($ET2);
$em->persist($y);
$em->flush();
//return $this->redirectToRoute('homepage');
}
}
}
}
return $this->render('default/index.html.twig', array(
'form' => $form->createView(),
'greet' => $greet,
'selection1' => $selection1,
'eT1Name' => $eT1Name,
'selection2' => $selection2,
'eT2Name' => $eT2Name,
));
}
}
Below is the events entity:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
//use Symfony\Component\Validator\Constraints as Assert;
/**
* events
*
* #ORM\Table(name="events")
* #ORM\Entity(repositoryClass="AppBundle\Repository\eventsRepository")
*/
class events {
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var int
*
* #ORM\Column(name="ET1", type="integer")
*/
protected $eT1;
/**
* #var int
*
* #ORM\Column(name="ET2", type="integer")
*/
protected $eT2;
/**
* #ORM\OneToOne(targetEntity="users", inversedBy="event")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set user
*
* #param users $user
* #return events
*/
public function setUser($user) {
$this->user = $user;
return $this;
}
/**
* Get user
*
* #return events
*/
public function getUser() {
return $this->user;
}
/**
* Set eT1
*
* #param integer $eT1
* #return events
*/
public function setET1($eT1) {
$this->eT1 = $eT1;
return $this;
}
/**
* Get eT1
*
* #return integer
*/
public function getET1() {
return $this->eT1;
}
/**
* Set eT2
*
* #param integer $eT2
* #return events
*/
public function setET2($eT2) {
$this->eT2 = $eT2;
return $this;
}
/**
* Get eT2
*
* #return integer
*/
public function getET2() {
return $this->eT2;
}
}
But when I shifted the code for Form creation in the eventsType.php, The following error has been showing up - A form can only be submitted once 500 Internal Server Error - AlreadySubmittedException
Here is the new controller code:
<?php
namespace AppBundle\Controller;
use AppBundle\Entity\events;
use AppBundle\Form\eventsType;
use AppBundle\Entity\users;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class DefaultController extends Controller {
/**
* #Route("/home", name="homepage")
*/
public function indexAction(Request $request) {
$greet = 'Welcome to the Birthday Party registration!';
$selection1 = '';
$selection2 = '';
//get the events repository
$repository = $this->getDoctrine()->getRepository('AppBundle:events');
//get the user_id of the logged in user
$user = $this->container->get('security.context')->getToken()->getUser()->getID();
//check if the user has already registered or not
$regEvents = $repository->findOneBy(array('user' => $user));
$events = new events();
//create form
$form = $this->createForm(new \AppBundle\Form\eventsType($regEvents), $events);
$form->handleRequest($request);
//retrieve the choices array for eT1 and eT2
$eT1Choices = $form->get('eT1')->getConfig()->getOption('choices');
$eT2Choices = $form->get('eT2')->getConfig()->getOption('choices');
//intialize the eventname variables
$eT1Name = '';
$eT2Name = '';
if ($regEvents) {
$et1 = $regEvents->getET1();
$et2 = $regEvents->getET2();
//If the user has registered already, display his previously selected options
$selection1 = 'Your After Breakfast event:';
$selection2 = 'Your After Snacks event:';
//set the eventname based on the value of et1
foreach ($eT1Choices as $key => $value) {
if ($et1 == $value) {
$eT1Name = $key;
}
}
//set the eventname based on the value of et2
foreach ($eT2Choices as $key => $value) {
if ($et2 == $value) {
$eT2Name = $key;
}
}
}
//after submission
if ($request->isMethod('POST')) {
$form->submit($request);
//First check the value entered by the user
if ($events->getET1() == null || $events->getET2() == null) {
//User did not choose both the events
$this->container->get('session')->getFlashBag()->add('error', 'Oh oh! It is mandatory to choose an option for all the events');
//return array('form' => $form->createView());
}
//retrieve maxlimit parameters from parameters.yml
$maxPoker = $this->container->getParameter('pokermaxlimit');
$maxChess = $this->container->getParameter('chessmaxlimit');
$maxCricket = $this->container->getParameter('cricketmaxlimit');
$maxMarbles = $this->container->getParameter('marblesmaxlimit');
$maxFootball = $this->container->getParameter('footballmaxlimit');
//initialize $eventMaxLim
$eventMaxLim1 = 0;
$eventMaxLim2 = 0;
//retrieve form data
$formData = $form->getData();
$ET1 = $formData->getET1();
$ET2 = $formData->getET2();
$selection1 = 'Your After Breakfast event:';
$selection2 = 'Your After Snacks event:';
//set the eventname based on the value of eT1
foreach ($eT1Choices as $key => $value) {
if ($ET1 == $value) {
$eT1Name = $key;
}
}
//set the eventname based on the value of eT2
foreach ($eT2Choices as $key => $value) {
if ($ET2 == $value) {
$eT2Name = $key;
}
}
//check to see if the user has registered the same event for eT1 and eT2
if ($ET1 == $ET2) {
$this->get('session')->getFlashBag()->set('error', 'You have chosen same events for both time slots! Please choose different ones.');
}
//check to see how many users have registered for the opted event(eT1)
$query1 = $repository->createQueryBuilder('p')
->select('count(p)')
->where('p.eT1 = :eT1')
->setParameter('eT1', $ET1)
->getQuery();
$a = $query1->getSingleScalarResult();
//set the $eventMaxLim based on the chosen event for eT1
if ($ET1 == 1) {
$eventMaxLim1 = $maxPoker;
} else if ($ET1 == 2) {
$eventMaxLim1 = $maxChess;
} else if ($ET1 == 3) {
$eventMaxLim1 = $maxCricket;
} else if ($ET1 == 4) {
$eventMaxLim1 = $maxMarbles;
} else if ($ET1 == 5) {
$eventMaxLim1 = $maxFootball;
}
// var_dump($eventMaxLim1);
// exit;
//check to see if the after breakfast event (eT1) is full (ie.has reached the maxlimit)
if ($a >= $eventMaxLim1) {
$this->get('session')->getFlashBag()->set('error', 'choose another After Breakfast event, this one is full');
}
//check to see how many users have registered for the opted event(eT2)
$query2 = $repository->createQueryBuilder('p')
->select('count(p)')
->where('p.eT2 = :eT2')
->setParameter('eT2', $ET2)
->getQuery();
$b = $query2->getSingleScalarResult();
//set the $eventMaxLim based on the chosen event for eT2
if ($ET2 == 1) {
$eventMaxLim2 = $maxPoker;
} else if ($ET2 == 2) {
$eventMaxLim2 = $maxChess;
} else if ($ET2 == 3) {
$eventMaxLim2 = $maxCricket;
} else if ($ET2 == 4) {
$eventMaxLim2 = $maxMarbles;
} else if ($ET2 == 5) {
$eventMaxLim2 = $maxFootball;
}
//check to see if the after snacks event (eT2) is full (ie.has reached the maxlimit)
if ($b >= $eventMaxLim2) {
$this->get('session')->getFlashBag()->set('error', 'choose another After Snacks event, this one is full');
}
if (($a < $eventMaxLim1) && ($b < $eventMaxLim2) && ($ET1 != $ET2) && ($events->getET1() == null ||
$events->getET2() == null)) {
if ($form->isValid()) {
//get the entity manager
$em = $this->getDoctrine()->getManager();
// If the user is registering for the first time (execute the Insert query)
if (!$regEvents) {
$events->setUser($user);
$events->setET1($ET1);
$events->setET2($ET2);
$em->persist($events);
$em->flush();
//return $this->redirectToRoute('homepage');
}
//If the user has registered already and want change his registered events (execute the Update query)
else {
$events->setET1($ET1);
$events->setET2($ET2);
$em->persist($events);
$em->flush();
//return $this->redirectToRoute('homepage');
}
}
}
}
return $this->render('default/index.html.twig', array(
'form' => $form->createView(),
'greet' => $greet,
'selection1' => $selection1,
'eT1Name' => $eT1Name,
'selection2' => $selection2,
'eT2Name' => $eT2Name,
));
}
}
Below is the eventsType.php:
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class eventsType extends AbstractType {
protected $events;
public function __construct($events) {
$this->events = $events;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
if (!empty($this->events)){
if($this->events->getET1() == null){
$et1 = '';
}
else {
$et1 = $this->events->getET1();
}
if($this->events->getET2() == null){
$et2 = '';
}
else {
$et2 = $this->events->getET2();
}
}
else {
$et1 = '';
$et2 = '';
}
$builder->add('eT1', ChoiceType::class, array(
'choices' => array(
'Poker' => 1,
'Chess' => 2,
'Cricket' => 3,
'Marbles' => 4,
'Football' => 5,
),
'choices_as_values' => true,
'expanded' => true,
'multiple' => false,
'label' => 'Choose After Breakfast Event',
'data' => $et1,
// 'mapped' => $map1,
))
->add('eT2', ChoiceType::class, array(
'choices' => array(
'Poker' => 1,
'Chess' => 2,
'Cricket' => 3,
'Marbles' => 4,
'Football' => 5,
),
'choices_as_values' => true,
'expanded' => true,
'multiple' => false,
'label' => 'Choose After Snacks Event',
'data' => $et2,
// 'mapped' => $map2,
))
->add('save', SubmitType::class, array('label' => 'Submit'));
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\events',
));
}
}
remove $form->submit($request); from your code and that should stop this error message.
$form->submit($request); (now deprecated) is used to submit the form from your controller, in your case you're using
$form->handleRequest($request); AND $form->submit($request); so as soon as the user presses the submit button submit() is called which again attempts to submit the form and hence the error message "A form can only be submitted once" link to docs
side note:
if ($events->getET1() == null || $events->getET2() == null) {
//User did not choose both the events
$this->container->get('session')->getFlashBag()->add('error', 'msg');
//return array('form' => $form->createView());
}
this if condition can and should be replaced by asserts docs and while flash messages are awesome and very useful this is not the place to use it, you may have misunderstood the usage of it, its used to display success and failure messages after form submission, NOT for validation messages, we have assert messages for that
and also read about MVC patterns and 'separation of concerns', divide your code into parts, one with code that displays content to the user, one that interacts with your database, one that does all the logical processing, the controller is definitely NOT the place to do any of this. the controller must be small most of the time and must make use of the built in functionalities of the framework.
i would suggest to take a break from writing codes and start reading about software architecture, and design patterns, with the current way i dont see much space for progress
-dheeraj

Elasticsearch in laravel 5.1

I want to integrate elasticsearch in my laravel project.
I have installed using following line :
Run command on terminal :
composer require shift31/laravel-elasticsearch:~1.0
Then i have created elasticsearch.php in app/config/ and added following code.
<?php
use Monolog\Logger;
return array(
'hosts' => array(
'your.elasticsearch.server:9200' // what should be my host ?
),
'logPath' => 'path/to/your/elasticsearch/log',
'logLevel' => Logger::INFO
);
My first question : What should i write in place of host name
Right now my project is running on local server with localhost:8000.
I have added Shift31\LaravelElasticsearch\ElasticsearchServiceProvider in app/config/app.php for enable the 'Es' facade.
Above all things done. Now in which file i should add the code of elasticsearch to add, update, delete and search the records.
I have product table I need to add product records in elasticsearch, when update product, records should be update.
I have no idea of the further process. Please guide me I have searched on google but no any example help me.
Create the following helper classes in their respective paths:
App\Traits\ElasticSearchEventTrait.php
<?php
Namespace App\Traits;
trait ElasticSearchEventTrait {
public $esRemoveDefault = array('created_at','updated_at','deleted_at');
public static function boot()
{
parent::boot();
static::bootElasticSearchEvent();
}
public static function bootElasticSearchEvent()
{
static::created(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esCreate();
}
});
static::updated(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esUpdate();
}
});
static::deleted(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esUpdate();
}
});
}
private function esCreate()
{
//esContext is false for polymorphic relations with no elasticsearch indexing
if(isset($this->esMain) && $this->esMain === true && $this->esContext !== false)
{
\Queue::push('ElasticSearchHelper#indexTask',array('id'=>$this->esGetId(),'class'=>get_class($this),'context'=>$this->esGetContext(),'info-context'=>$this->esGetInfoContext(),'excludes'=>$this->esGetRemove()));
}
else
{
$this->esUpdate();
}
}
private function esUpdate()
{
//esContext is false for polymorphic relations with no elasticsearch indexing
if($this->esContext !== false)
{
\Queue::push('ElasticSearchHelper#updateTask',array('id'=>$this->esGetId(),'class'=>get_class($this),'context'=>$this->esGetContext(),'info-context'=>$this->esGetInfoContext(),'excludes'=>$this->esGetRemove()));
}
}
/*
* Get Id of Model
*/
public function esGetId()
{
if(isset($this->esId))
{
return $this->esId;
}
else
{
return $this->id;
}
}
public function esGetInfoContext()
{
if(isset($this->esInfoContext))
{
return $this->esInfoContext;
}
else
{
throw new \RuntimeException("esInfoContext attribute or esGetInfoContext() is not set in class '".get_class($this)."'");
}
}
/*
* Name of main context of model
*/
public function esGetContext()
{
if(isset($this->esContext))
{
return $this->esContext;
}
else
{
throw new \RuntimeException("esContext attribute or esGetContext() method must be set in class '".get_class($this)."'");
}
}
/*
* All attributes that needs to be removed from model
*/
public function esGetRemove()
{
if(isset($this->esRemove))
{
return array_unique(array_merge($this->esRemoveDefault,$this->esRemove));
}
else
{
return $this->esRemoveDefault;
}
}
/*
* Extends Illuminate Collection to provide additional array functions
*/
public function newCollection(array $models = Array())
{
return new Core\Collection($models);
}
/**
* Return a timestamp as DateTime object.
*
* #param mixed $value
* #return \Carbon\Carbon
*/
public function asEsDateTime($value)
{
// If this value is an integer, we will assume it is a UNIX timestamp's value
// and format a Carbon object from this timestamp. This allows flexibility
// when defining your date fields as they might be UNIX timestamps here.
if (is_numeric($value))
{
return \Carbon::createFromTimestamp($value);
}
// If the value is in simply year, month, day format, we will instantiate the
// Carbon instances from that format. Again, this provides for simple date
// fields on the database, while still supporting Carbonized conversion.
elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value))
{
return \Carbon::createFromFormat('Y-m-d', $value)->startOfDay();
}
// Finally, we will just assume this date is in the format used by default on
// the database connection and use that format to create the Carbon object
// that is returned back out to the developers after we convert it here.
elseif ( ! $value instanceof DateTime)
{
$format = $this->getEsDateFormat();
return \Carbon::createFromFormat($format, $value);
}
return \Carbon::instance($value);
}
/**
* Get the format for database stored dates.
*
* #return string
*/
private function getEsDateFormat()
{
return $this->getConnection()->getQueryGrammar()->getDateFormat();
}
/*
* Converts model to a suitable format for ElasticSearch
*/
public function getEsSaveFormat()
{
$obj = clone $this;
//Go through ES Accessors
\ElasticSearchHelper::esAccessor($obj);
$dates = $this->getDates();
//Convert to array, then change Date to appropriate Elasticsearch format.
//Why? Because eloquent's date accessors is playing me.
$dataArray = $obj->attributesToArray();
//Remove all Excludes
foreach($this->esGetRemove() as $ex)
{
if(array_key_exists($ex,$dataArray))
{
unset($dataArray[$ex]);
}
}
if(!empty($dates))
{
foreach($dates as $d)
{
if(isset($dataArray[$d]) && $dataArray[$d] !== "" )
{
//Trigger Eloquent Getter which will provide a Carbon instance
$dataArray[$d] = $this->{$d}->toIso8601String();
}
}
}
return $dataArray;
}
}
App\Services\ElasticServiceHelper.php
<?php
/**
* Description of ElasticSearchHelper: Helps with Indexing/Updating with Elastic Search Server (https://www.elastic.co)
*
* #author kpudaruth
*/
Namespace App\Services;
class ElasticSearchHelper {
/*
* Laravel Queue - Index Task
* #param array $job
* #param array $data
*/
public function indexTask($job,$data)
{
if(\Config::get('website.elasticsearch') === true)
{
if(isset($data['context']))
{
$this->indexEs($data);
}
else
{
\Log::error('ElasticSearchHelper: No context set for the following dataset: '.json_encode($data));
}
}
$job->delete();
}
/*
* Laravel Queue - Update Task
* #param array $job
* #param array $data
*/
public function updateTask($job,$data)
{
if(\Config::get('website.elasticsearch') === true)
{
if(isset($data['context']))
{
$this->updateEs($data);
}
else
{
\Log::error('ElasticSearchHelper: No context set for the following dataset: '.json_encode($data));
}
}
$job->delete();
}
/*
* Index Elastic Search Document
* #param array $data
*/
public function indexEs($data)
{
$params = array();
$params['index'] = \App::environment();
$params['type'] = $data['context'];
$model = new $data['class'];
$form = $model::find($data['id']);
if($form)
{
$params['id'] = $form->id;
if($form->timestamps)
{
$params['timestamp'] = $form->updated_at->toIso8601String();
}
$params['body'][$data['context']] = $this->saveFormat($form);
\Es::index($params);
}
}
/*
* Update Elastic Search
* #param array $data
*/
public function updateEs($data)
{
$params = array();
$params['index'] = \App::environment();
$params['type'] = $data['context'];
$model = new $data['class'];
$form = $model::withTrashed()->find($data['id']);
if(count($form))
{
/*
* Main form is being updated
*/
if($data['info-context'] === $data['context'])
{
$params['id'] = $data['id'];
$params['body']['doc'][$data['info-context']] = $this->saveFormat($form);
}
else
{
//Form is child, we get parent
$parent = $form->esGetParent();
if(count($parent))
{
//Id is always that of parent
$params['id'] = $parent->id;
//fetch all children, given that we cannot save per children basis
$children = $parent->{$data['info-context']}()->get();
if(count($children))
{
//Get data in a format that can be saved by Elastic Search
$params['body']['doc'][$data['info-context']] = $this->saveFormat($children);
}
else
{
//Empty it is
$params['body']['doc'][$data['info-context']] = array();
}
}
else
{
\Log::error("Parent not found for {$data['context']} - {$data['class']}, Id: {$data['id']}");
return false;
}
}
//Check if Parent Exists
try
{
$result = \Es::get([
'id' => $params['id'],
'index' => $params['index'],
'type' => $data['context']
]);
} catch (\Exception $ex) {
if($ex instanceof \Elasticsearch\Common\Exceptions\Missing404Exception || $ex instanceof \Guzzle\Http\Exception\ClientErrorResponseException)
{
//if not, we set it
if (isset($parent) && $parent)
{
$this->indexEs([
'context' => $data['context'],
'class' => get_class($parent),
'id' => $parent->id,
]);
}
else
{
\Log::error('Unexpected error in updating elasticsearch records, parent not set with message: '.$ex->getMessage());
return false;
}
}
else
{
\Log::error('Unexpected error in updating elasticsearch records: '.$ex->getMessage());
return false;
}
}
\Es::update($params);
}
}
/*
* Iterate through all Es accessors of the model.
* #param \Illuminate\Database\Eloquent\Model $object
*/
public function esAccessor(&$object)
{
if(is_object($object))
{
$attributes = $object->getAttributes();
foreach($attributes as $name => $value)
{
$esMutator = 'get' . studly_case($name) . 'EsAttribute';
if (method_exists($object, $esMutator)) {
$object->{$name} = $object->$esMutator($object->{$name});
}
}
}
else
{
throw New \RuntimeException("Expected type object");
}
}
/*
* Iterates over a collection applying the getEsSaveFormat function
* #param mixed $object
*
* #return array
*/
public function saveFormat($object)
{
if($object instanceof \Illuminate\Database\Eloquent\Model)
{
return $object->getEsSaveFormat();
}
else
{
return array_map(function($value)
{
return $value->getEsSaveFormat();
}, $object->all());
}
}
}
A couple of gotchas from the above helper classes:
The default ElasticSearch index is set to the name of the App's Environment
The ..task() functions are meant for the old laravel 4.2 queue format. I've yet to port those to laravel 5.x. Same goes for the Queue::push commands.
Example
ElasticSearch Mapping:
[
'automobile' => [
"dynamic" => "strict",
'properties' => [
'automobile' => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'manufacturer_name' => [
'type' => 'string',
],
'manufactured_on' => [
'type' => 'date'
]
]
],
'car' => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'name' => [
'type' => 'string',
],
'model_id' => [
'type' => 'string'
]
]
],
"car-model" => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'description' => [
'type' => 'string',
],
'name' => [
'type' => 'string'
]
]
]
]
]
]
Top level document is called 'automobile'. Underneath it, you have 'automobile', 'car' & 'car-model'. Consider 'car' & 'car-model' as relations to the automobile. They are known as sub documents on elasticsearch. (See: https://www.elastic.co/guide/en/elasticsearch/guide/current/document.html)
Model: App\Models\Car.php
namespace App\Models;
class Car extends \Eloquent {
use \Illuminate\Database\Eloquent\SoftDeletingTrait;
use \App\Traits\ElasticSearchEventTrait;
protected $table = 'car';
protected $fillable = [
'name',
'serie',
'model_id',
'automobile_id'
];
protected $dates = [
'deleted_at'
];
/* Elastic Search */
//Indexing Enabled
public $esEnabled = true;
//Context for Indexing - Top Level name in the mapping
public $esContext = "automobile";
//Info Context - Secondary level name in the mapping.
public $esInfoContext = "car";
//The following fields will not be saved in elasticsearch.
public $esRemove = ['automobile_id'];
//Fetches parent relation of car, so that we can retrieve its id for saving in the appropriate elasticsearch record
public function esGetParent()
{
return $this->automobile;
}
/*
* Event Observers
*/
public static function boot() {
parent:: boot();
//Attach events to model on start
static::bootElasticSearchEvent();
}
/*
* ElasticSearch Accessor
*
* Sometimes you might wish to format the data before storing it in elasticsearch,
* The accessor name is in the format of: get + attribute's name camel case + EsAttribute
* The $val parameter will always be the value of the attribute that is being accessed.
*
* #param mixed $val
*/
/*
* Elasticsearch Accessor: Model Id
*
* Get the model name and save it
*
* #param int $model_id
* #return string
*/
public function getModelIdEsAttribute($model_id) {
//Fetch model from table
$model = \App\Models\CarModel::find($model_id);
if($model) {
//Return name of model if found
return $model->name;
} else {
return '';
}
}
/*
* Automobile Relationship: Belongs To
*/
public function automobile()
{
return $this->belongsTo('\App\Models\Automobile','automobile_id');
}
}
Example of Search Query:
/**
* Get search results
*
* #param string $search (Search string)
*
*/
public function getAll($search)
{
$params = array();
$params['index'] = App::environment();
//Declare your mapping names in the array which you wish to search on.
$params['type'] = array('automobile');
/*
* Build Query String
*/
//Exact match is favored instead of fuzzy ones
$params['body']['query']['bool']['should'][0]['match']['name']['query'] = $search;
$params['body']['query']['bool']['should'][0]['match']['name']['operator'] = "and";
$params['body']['query']['bool']['should'][0]['match']['name']['boost'] = 2;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['like_text'] = $search;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['fuzziness'] = 0.5;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['prefix_length'] = 2;
$params['body']['query']['bool']['minimum_should_match'] = 1;
//Highlight matches
$params['body']['highlight']['fields']['*'] = new \stdClass();
$params['body']['highlight']['pre_tags'] = array('<b>');
$params['body']['highlight']['post_tags'] = array('</b>');
//Exclude laravel timestamps
$params['body']['_source']['exclude'] = array( "*.created_at","*.updated_at","*.deleted_at");
/*
* Poll search server until we have some results
*/
$from_offset = 0;
$result = array();
//Loop through all the search results
do
{
try
{
$params['body']['from'] = $from_offset;
$params['body']['size'] = 5;
$queryResponse = \Es::search($params);
//Custom function to process the result
//Since we will receive a bunch of arrays, we need to reformat the data and display it properly.
$result = $this->processSearchResult($queryResponse);
$from_offset+= 5;
}
catch (\Exception $e)
{
\Log::error($e->getMessage());
return Response::make("An error occured with the search server.",500);
}
}
while (count($result) === 0 && $queryResponse['hits']['total'] > 0);
echo json_encode($result);
}
/*
* Format search results as necessary
* #param array $queryResponse
*/
private function processSearchResult(array $queryResponse)
{
$result = array();
//Check if we have results in the array
if($queryResponse['hits']['total'] > 0 && $queryResponse['timed_out'] === false)
{
//Loop through each result
foreach($queryResponse['hits']['hits'] as $line)
{
//Elasticsearch will highlight the relevant sections in your query in an array. The below creates a readable format with · as delimiter.
$highlight = "";
if(isset($line['highlight']))
{
foreach($line['highlight'] as $k=>$v)
{
foreach($v as $val)
{
$highlight[] = str_replace("_"," ",implode(" - ",explode(".",$k)))." : ".$val;
}
}
$highlight = implode(" · ",$highlight);
}
//Check the mapping type
switch($line['_type'])
{
case "automobile":
$result[] = array('icon'=>'fa-automobile',
'title'=> 'Automobile',
'id' => $line['_id'],
//name to be displayed on my search result page
'value'=>$line['_source'][$line['_type']]['name']." (Code: ".$line['_id'].")",
//Using a helper to generate the url. Build your own class.
'url'=>\App\Helpers\URLGenerator::generate($line['_type'],$line['_id']),
//And the highlights as formatted above.
'highlight'=>$highlight);
break;
}
}
}
return $result;
}

Categories