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;
}
Related
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());
}
This piece of code shows a smll part of the models post.php from October Rainlab Blog plugin. The AfterSave() function is modified, it sends an e-mail when a new blogPost in the backend is saved by the administrator, however, I would like to send it when it is actually Published and make sure it is not sending multiple times. How could I accomplish this?
public function filterFields($fields, $context = null)
{
if (!isset($fields->published, $fields->published_at)) {
return;
}
$user = BackendAuth::getUser();
if (!$user->hasAnyAccess(['rainlab.blog.access_publish'])) {
$fields->published->hidden = true;
$fields->published_at->hidden = true;
}
else {
$fields->published->hidden = false;
$fields->published_at->hidden = false;
}
}
public function afterValidate()
{
if ($this->published && !$this->published_at) {
throw new ValidationException([
'published_at' => Lang::get('rainlab.blog::lang.post.published_validation')
]);
}
}
public function beforeSave()
{
if (empty($this->user)) {
$user = BackendAuth::getUser();
if (!is_null($user)) {
$this->user = $user->id;
}
}
$this->content_html = self::formatHtml($this->content);
}
public function afterSave()
{
$user = BackendAuth::getUser();
if ($user && $user->hasAnyAccess(['rainlab.blog.access_publish'])) {
$susers = Db::select('select * from users where is_activated = ?', [1]);
foreach ($susers as $suser) {
$currentPath = $_SERVER['PHP_SELF'];
$pathInfo = pathinfo($currentPath);
$hostName = $_SERVER['HTTP_HOST'];
$protocol = strtolower(substr($_SERVER["SERVER_PROTOCOL"],0,5))=='https'?'https':'http';
$protocol.'://'.$hostName.$pathInfo['dirname']."/";
$spost_url = $protocol.'://'.$hostName.$pathInfo['dirname']."/"."nieuws/".$this->attributes['slug'] ;
$stitle = $this->attributes['title'] ;
$body = '<div> Hallo '.$suser->name.'</br> Er is zojuist een nieuws bericht gepubliceerd voor alle leden van mycompany.nl , je kunt hier het bericht lezen aangaande: '.$stitle.' </div>' ;
//$from = $user->email ;
$from = 'noreply#mycompany.nl';
$headers = "From: $from\r\n";
$headers .= "Content-type: text/html\r\n";
mail($suser->email,'Nieuws van mycompany', $body,$headers);
}
}
}
/**
* Sets the "url" attribute with a URL to this object.
* #param string $pageName
* #param Controller $controller
* #param array $params Override request URL parameters
*
* #return string
*/
public function setUrl($pageName, $controller, $params = [])
{
$params = array_merge([
'id' => $this->id,
'slug' => $this->slug,
], $params);
if (empty($params['category'])) {
$params['category'] = $this->categories->count() ? $this->categories->first()->slug : null;
}
// Expose published year, month and day as URL parameters.
if ($this->published) {
$params['year'] = $this->published_at->format('Y');
$params['month'] = $this->published_at->format('m');
$params['day'] = $this->published_at->format('d');
}
return $this->url = $controller->pageUrl($pageName, $params);
}
/**
* Used to test if a certain user has permission to edit post,
* returns TRUE if the user is the owner or has other posts access.
* #param User $user
* #return bool
*/
public function canEdit(User $user)
{
return ($this->user_id == $user->id) || $user->hasAnyAccess(['rainlab.blog.access_other_posts']);
}
public static function formatHtml($input, $preview = false)
{
$result = Markdown::parse(trim($input));
// Check to see if the HTML should be cleaned from potential XSS
$user = BackendAuth::getUser();
if (!$user || !$user->hasAccess('backend.allow_unsafe_markdown')) {
$result = Html::clean($result);
}
if ($preview) {
$result = str_replace('<pre>', '<pre class="prettyprint">', $result);
}
$result = TagProcessor::instance()->processTags($result, $preview);
return $result;
}
//
// Scopes
//
public function scopeIsPublished($query)
{
return $query
->whereNotNull('published')
->where('published', true)
->whereNotNull('published_at')
->where('published_at', '<', Carbon::now())
;
}
/**
* Lists posts for the frontend
*
* #param $query
* #param array $options Display options
* #return Post
*/
public function scopeListFrontEnd($query, $options)
{
/*
* Default options
*/
extract(array_merge([
'page' => 1,
'perPage' => 30,
'sort' => 'created_at',
'categories' => null,
'exceptCategories' => null,
'category' => null,
'search' => '',
'published' => true,
'exceptPost' => null
], $options));
$searchableFields = ['title', 'slug', 'excerpt', 'content'];
if ($published) {
$query->isPublished();
}
One way to accomplish this would be to extend the Post model.
As an example, you create a new plugin and model with an is_notified field.
You would then add something like this to the boot() method of your new plugin:
PostModel::extend(function ($model) {
$model->hasOne['your_model'] = ['Author\PluginName\Models\YourModel'];
});
PostsController::extendFormFields(function ($form, $model, $context) {
// Checking for Post instance
if (!$model instanceof PostModel) {
return;
}
// without this code you can get an error saying "Call to a member function hasRelation() on null"
if (!$model->your_model) {
$model->your_model = new YourModel;
}
}
You can then use that new model in the afterSave method
public function afterSave()
{
$user = BackendAuth::getUser();
if ($user && $user->hasAnyAccess(['rainlab.blog.access_publish'])) {
$susers = Db::select('select * from users where is_activated = ?', [1]);
foreach ($susers as $suser) {
...
if ($this->your_model->is_notified != true) {
mail($suser->email,'Nieuws van mycompany', $body,$headers);
$this->your_model->is_notified = true;
}
}
}
}
You should also consider using the extend method instead of modifying 3rd party plugin code. This will allow you to update the plugin without losing your edits. Something like this:
PostModel::extend(function ($model) {
$model->hasOne['your_model'] = ['Author\PluginName\Models\YourModel'];
// You can transfer your afterSave code here!
$model->bindEvent('model.afterSave', function () use ($model) {
$user = BackendAuth::getUser();
if ($user && $user->hasAnyAccess(['rainlab.blog.access_publish'])) {
..
}
});
});
Let me know if you have any questions!
I know, this is a complex case but maybe one of you might have an idea on how to do this.
Concept
I have the following process in my API:
Process query string parameters (FormRequest)
Replace key aliases by preferred keys
Map string parameters to arrays if an array ist expected
Set defaults (including Auth::user() for id-based parameters)
etc.
Check if the user is allowed to do the request (Middleware)
Using processed (validated and sanitized) query params
→ otherwise I had to do exceptions for every possible alias and mapping as well as checking if the paramter is checked and that doesn't seem reasonable to me.
Problem
Nevertheless, if you just assign the middleware via ->middleware('middlewareName') to the route and the FormRequest via dependency injection to the controller method, first the middleware is called and after that the FormRequest. As described above, that's not what I need.
Solution approach
I first tried dependency injection at the middleware but it didn't work.
My solution was to assign the middleware in the controller constructor. Dependency injection works here, but suddenly Auth::user() returns null.
Then, I came across the FormRequest::createFrom($request) method in \Illuminate\Foundation\Providers\FormRequestServiceProvider.php:34 and the possibility to pass the $request object to the middleware's handle() method. The result looks like this:
public function __construct(Request $request)
{
$middleware = new MyMiddleware();
$request = MyRequest::createFrom($request);
$middleware->handle($request, function() {})
}
But now the request is not validated yet. Just calling $request->validated() returns nothing. So I digged a little deeper and found that $resolved->validateResolved(); is done in \Illuminate\Foundation\Providers\FormRequestServiceProvider.php:30 but that doesn't seem to trigger the validation since it throws an exception saying that this method cannot be called on null but $request isn't null:
Call to a member function validated() on null
Now, I'm completely stumped. Does anyone know how to solve this or am I just doing it wrong?
Thanks in advance!
I guess, I figured out a better way to do this.
My misconception
While middleware is doing authentication, I was doing authorization there and therefore I have to use a Gate
Resulting code
Controller
...
public function getData(MyRequest $request)
{
$filters = $request->query();
// execute queries
}
...
FormRequest
class MyRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return Gate::allows('get-data', $this);
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
// ...
];
}
/**
* Prepare the data for validation.
*
* #return void
*/
protected function prepareForValidation()
{
$this->replace($this->cleanQueryParameters($this->query()));
}
private function cleanQueryParameters($queryParams): array
{
$queryParams = array_filter($queryParams, function($param) {
return is_array($param) ? count($param) : strlen($param);
});
$defaultStartDate = (new \DateTime())->modify('monday next week');
$defaultEndDate = (new \DateTime())->modify('friday next week');
$defaults = [
'article.created_by_id' => self::getDefaultEmployeeIds(),
'date_from' => $defaultStartDate->format('Y-m-d'),
'date_to' => $defaultEndDate->format('Y-m-d')
];
$aliases = [
// ...
];
$mapper = [
// ...
];
foreach($aliases as $alias => $key) {
if (array_key_exists($alias, $queryParams)) {
$queryParams[$key] = $queryParams[$alias];
unset($queryParams[$alias]);
}
}
foreach($mapper as $key => $fn) {
if (array_key_exists($key, $queryParams)) {
$fn($queryParams, $key);
}
}
$allowedFilters = array_merge(
Ticket::$allowedApiParameters,
array_map(function(string $param) {
return 'article.'.$param;
}, TicketArticle::$allowedApiParameters)
);
$arrayProps = [
// ..
];
foreach($queryParams as $param => $value) {
if (!in_array($param, $allowedFilters) && !in_array($param, ['date_from', 'date_to'])) {
abort(400, 'Filter "'.$param.'" not found');
}
if (in_array($param, $arrayProps)) {
$queryParams[$param] = guarantee('array', $value);
}
}
return array_merge($defaults, $queryParams);
}
}
Gate
class MyGate
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Auth\Access\Response|Void
* #throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function authorizeGetDataCall(User $user, MyRequest $request): Response
{
Log::info('[MyGate] Checking permissions …');
if (in_array(LDAPGroups::Admin, session('PermissionGroups', []))) {
// no further checks needed
Log::info('[MyGate] User is administrator. No further checks needed');
return Response::allow();
}
if (
($request->has('group') && !in_array(Group::toLDAPGroup($request->get('group')), session('PermissionGroups', []))) ||
$request->has('owner.department') && !in_array(Department::toLDAPGroup($request->query('owner.department')), session('PermissionGroups', [])) ||
$request->has('creator.department') && !in_array(Department::toLDAPGroup($request->query('creator.department')), session('PermissionGroups', []))
) {
Log::warning('[MyGate] Access denied due to insufficient group/deparment membership', [ 'group/department' =>
$request->has('group') ?
Group::toLDAPGroup($request->get('group')) :
($request->has('owner.department') ?
Department::toLDAPGroup($request->query('owner.department')) :
($request->has('creator.department') ?
Department::toLDAPGroup($request->query('creator.department')) :
null))
]);
return Response::deny('Access denied');
}
if ($request->has('customer_id') || $request->has('article.created_by_id')) {
$ids = [];
if ($request->has('customer_id')) {
$ids = array_merge($ids, $request->query('customer_id'));
}
if ($request->has('article.created_by_id')) {
$ids = array_merge($ids, $request->query('article.created_by_id'));
}
$users = User::find($ids);
$hasOtherLDAPGroup = !$users->every(function($user) {
return in_array(Department::toLDAPGroup($user->department), session('PermissionGroups', []));
});
if ($hasOtherLDAPGroup) {
Log::warning('[MyGate] Access denied due to insufficient permissions to see specific other user\'s data', [ 'ids' => $ids ]);
return Response::deny('Access denied');;
}
}
if ($request->has('owner.login') || $request->has('creator.login')) {
$logins = [];
if ($request->has('owner.login')) {
$logins = array_merge(
$logins,
guarantee('array', $request->query('owner.login'))
);
}
if ($request->has('creator.login')) {
$logins = array_merge(
$logins,
guarantee('array', $request->query('creator.login'))
);
}
$users = User::where([ 'samaccountname' => $logins ])->get();
$hasOtherLDAPGroup = !$users->every(function($user) {
return in_array(Department::toLDAPGroup($user->department), session('PermissionGroups', []));
});
if ($hasOtherLDAPGroup) {
Log::warning('[MyGate] Access denied due to insufficient permissions to see specific other user\'s data', [ 'logins' => $logins ]);
return Response::deny('Access denied');
}
}
Log::info('[MyGate] Permission checks passed');
return Response::allow();
}
}
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.
Hi I am using a SAutoComplete (extends CAutoComplete) and need to do some work when a value is selected from the list.
i am using it like this.
this->widget('application.components.SAutoComplete', array('width'=>200,
'model'=>$cssAtapsClient, 'parseData'=>true, 'matchContains'=>true,
'attribute'=>'suburb_id', 'data'=>$postCode, 'ddindicator'=>true,
'max'=>50,
'multipleSeparator'=>false,
'options' => array(
'select' => new CJavaScriptExpression('function(e, ui) { alert("hi"); }')
),
)); ?>
i am wondering why there is no select option like available in jquery UI auto completed?
example of a select is as below.
$("#auto_cp").autocomplete({
minLength: 3,
//source
source: function(req, add) {
$.getJSON("friends.php?callback=?", req, function(data) {
var suggestions = [];
$.each(data, function(i, val) {
suggestions.push({
label: val.name,
zzz: val.zzz
});
});
add(suggestions);
});
},
//select
select: function(e, ui) {
alert(ui.item.zzz);
}
});
EDIT 2
http://www.yiiframework.com/doc/api/1.1/CAutoComplete
code is like this,
<?php
class SAutoComplete extends CAutoComplete
{
public $ddindicator;
/**
*
* #var boolean whether to parse the data assumes data uses an assoc array
* array(value => array(name, value, ...), ...). Only works with a model present
*/
public $parseData;
/**
* #var boolean whether to raise the change event
*/
public $raiseChangeEvent = false;
/**
* Initializes the widget.
* This method registers all needed client scripts and renders
* the autocomplete input.
*/
public function init()
{
if ( !$this->max )
$this->max = 50000;
if ( $this->ddindicator )
$this->alternateInit();
else
parent::init();
}
public function alternateInit()
{
list($name,$id)=$this->resolveNameID();
$this->htmlOptions['id'] = $id.'_input';
$this->minChars = 0;
echo CHtml::openTag('div', array('class'=>'ac-input-dd'));
echo CHtml::openTag('div', array('class'=>'ac-input-btn'));
echo CHtml::closeTag('div');
if($this->hasModel())
{
$htmlOpt = array();
if ( $this->parseData )
{
$menu = $this->data;
$key = $this->attribute;
//Change if attribute is apart of a array. eg attribute[0]
$pos1 = stripos($key, '[');
$pos2 = stripos($key, ']');
if($pos1!==false && $pos2!==false)
{
$key = str_replace (substr($key,$pos1,$pos2 - $pos1 + 1),'',$key);
$htmlOpt['value'] = isset($this->model->$key) ? $this->model->$key : '';
}
$this->value = isset($menu[$this->model->$key][0]) ? $menu[$this->model->$key][0] : '';
$this->data = is_array($menu) ? array_values($menu) : array('Error in data.');
}
echo CHtml::activeHiddenField($this->model, $this->attribute, array_merge(array('id'=>$id, 'name'=>$name), $htmlOpt));
echo CHtml::textField('', $this->value, $this->htmlOptions);
}
else
{
echo CHtml::hiddenField($name, $this->value, array('id'=>$id));
echo CHtml::textField($name.'_input',$this->value,$this->htmlOptions);
}
echo CHtml::closeTag('div');
$this->methodChain = $this->methodChain.'.result(function(evt, data, formatted) { $("#'.
$id.'").val(data ? data[1] : "")'.($this->raiseChangeEvent?'.change()':'').'; })'.
'.parent().find(".ac-input-btn").mousedown(function(){'.
'jQuery(this).parent().find(".ac_input").toggleResults();})'.
'.mouseup(function(){jQuery(this).parent().find(".ac_input").focus();});';
$this->registerClientScript();
}
public static function registerScript()
{
$cs = Yii::app()->getClientScript();
$cs->registerCoreScript('jquery');
$cs->registerCoreScript('bgiframe');
TK::registerScriptFile('autocomplete');
$cs->registerCssFile($cs->getCoreScriptUrl().'/autocomplete/jquery.autocomplete.css');
}
/**
* Registers the needed CSS and JavaScript.
* #since 1.0.1
*/
public function registerClientScript()
{
// can cut this down once YII releases a fix for defect #38
if ( Yii::app()->request->isAjaxRequest || $this->ddindicator )
{
$id=$this->htmlOptions['id'];
$acOptions=$this->getClientOptions();
$options=$acOptions===array()?'{}' : CJavaScript::encode($acOptions);
$cs=Yii::app()->getClientScript();
if($this->data!==null)
$data=CJavaScript::encode($this->data);
else
{
$url=CHtml::normalizeUrl($this->url);
$data='"'.$url.'"';
}
if ( Yii::app()->request->isAjaxRequest )
{
echo '<script type="text/javascript">jQuery(document).ready('.
'function() {jQuery("#'.$id.'").autocomplete('.$data.','.$options.')'.
$this->methodChain.';});</script>';
}
else
{
SAutoComplete::registerScript();
$cs->registerScript('Yii.CAutoComplete#'.$id,"jQuery(\"#{$id}\").autocomplete($data,{$options}){$this->methodChain};");
}
}
else
parent::registerClientScript();
}
}
You can pass any option that the JUI autocomplete widget supports by including an options array:
$this->widget('zii.widgets.jui.CJuiAutoComplete', array(
// your other settings here
'options' => array(
'select' => new CJavaScriptExpression('function(e, ui) { alert("hi"); }')
),
));
If the options you want to pass include JavaScript code then you also have to wrap that inside a CJavaScriptExpression as above.
Try to add this,
'methodChain'=>".result(function(event,item){ urFunction(); })",
Have a nice day!
So at last it will look like,
$this->widget('application.components.SAutoComplete', array('width'=>200,
'model'=>$cssAtapsClient, 'parseData'=>true, 'matchContains'=>true,
'attribute'=>'suburb_id', 'data'=>$postCode, 'ddindicator'=>true,
'max'=>50,
'methodChain'=>".result(function(event,item){ urFucntion(); })",
));
I'm using CAutoComplete as the followin code:
In post/_form.php
<?php $this->widget('CAutoComplete', array(
'model' => $model,
'attribute' => 'tags',
'url' => array('suggestTags'),
'multiple' => true,
'htmlOptions' => array(
'size' => 50,
'class' => 'span11'
),
)); ?>
In PostController.php
Add one more action call: suggestTags
/**
* Suggests tags based on the current user input.
* This is called via AJAX when the user is entering the tags input.
*/
public function actionSuggestTags()
{
if(isset($_GET['q']) && ($keyword=trim($_GET['q']))!=='')
{
$tags=Tag::model()->suggestTags($keyword);
if($tags!==array())
echo implode("\n",$tags);
}
}
In Post.php model
add private $_oldTags; to the top of class (under class name).
add these functions:
/**
* Normalizes the user-entered tags.
*/
public function normalizeTags($attribute, $params)
{
$this->tags = Post::array2string(array_unique(Post::string2array($this->tags)));
}
public static function string2array($tags)
{
return preg_split('/\s*,\s*/', trim($tags), -1, PREG_SPLIT_NO_EMPTY);
}
public static function array2string($tags)
{
return implode(', ', $tags);
}
See more Yii tutorials here