Laravel routing - custom domain for same routes - php

Say I have theese routes:
http://mylaravelapp.com/{slug}
http://mylaravelapp.com/{slug}/page
http://mylaravelapp.com/{slug}/another/{custom_slug}
And I would like the same routes to be accessed from a custom domain, pointing to my laravel app's IP. Like this:
http://customdomain.com/
http://customdomain.com/page
http://customdomain.com/another/{custom_slug}
How is this achieved the best way?
My current, ugly way
I have made my own solution, which is rather ugly. It involves a lot of repeat-code, and a nasty controller. This is how I achieve it:
routes.php
/**
* Serving from my app:
*/
Route::get('/{slug}', ['uses' => 'MyController#show', 'as' => 'page']);
Route::get('/{slug}/page', ['uses' => 'MyController#page', 'as' => 'page.page']);
Route::get('/{slug}/another/{custom_slug}', ['uses' => 'MyController#another', 'as' => 'page.another']);
/**
* Serving from custom domain
*/
Route::group(['domain' => '{custom_domain}.{tld}'], function($domain) {
Route::get('/', ['uses' => 'MyController#show', 'as' => 'page.customdomain']);
Route::get('/page', ['uses' => 'MyController#page']);
Route::get('/another/{custom_slug}', ['uses' => 'MyController#another']);
});
MyController.php
class MyController extends Controller {
/**
* Find by slug or custom domain
*
* #return [type] [description]
*/
public function findBySlugOrDomain($domain, $domain_tld, $slug) {
if($domain && $domain_tld) {
/**
* Find by custom domain name
*/
$page = $this->page->findByDomain($domain.'.'.$domain_tld)->firstOrFail();
} else {
/**
* Find by slug (no custom domain)
* #var [type]
*/
$page = $this->page->findBySlugOrFail($slug);
}
return $page;
}
/**
* Display the specified resource.
*/
public function show($domain = null, $domain_tld = null, $slug = null, $type = 'home', $custom_slug = null)
{
/**
* Cases
*/
if(str_contains(config('app.url'), $domain . '.' . $domain_tld)) {
/**
* Incoming request to HOME (i.e. http://mylaravelapp.com/)
*/
return app('App\Http\Controllers\HomeController')->index();
} elseif($domain && !$domain_tld) {
/**
* Request to page on http://mylaravelapp.com/{slug}/page
*/
$slug = $domain;
$domain = null;
$domain_tld = null;
} else if($domain && $domain_tld && !$slug) {
/**
* Request to page with slug on http://mylaravelapp.com/{slug}/another/{custom_slug}
*/
$slug = $domain;
$custom_slug = $domain_tld;
$domain = null;
$domain_tld = null;
} else if($domain && $domain_tld && $slug) {
/**
* Request to page on http://customdomain.com/
*/
} else if($domain && $domain_tld && $slug) {
/**
* Request to page with slug on http://customdomain.com/another/{custom_slug}
*/
$custom_slug = $slug;
}
$page = $this->findBySlugOrDomain($domain, $domain_tld, $slug);
switch ($type) {
case 'page':
return view('page.page', compact('page'));
break;
case 'another':
$anotherPage = $page->another()->whereSlug($custom_slug)->firstOrFail();
return view('page.another', compact('page', 'anotherPage'));
break;
}
return view('page.home', compact('page', 'js_variables'));
}
/**
* Page: page
*
* http://mylaravelapp.com/{slug}/page
*
* http://customdomain.com/page
*/
public function showPage($domain = null, $domain_tld = null, $slug = null) {
return $this->show($domain, $domain_tld, $slug, 'gallery');
}
/**
* Page: another
*
* http://mylaravelapp.com/{slug}/another/{custom_slug}
*
* http://customdomain.com/another/{custom_slug}
*/
public function showAnother($domain = null, $domain_tld = null, $slug = null, $custom_slug = null) {
return $this->show($domain, $domain_tld, $slug, 'page', $custom_slug);
}
}
Limitations of this way:
A lot of repeat code
Every time I add a new route, I need to update it twice
Long and in-understandable Controller
A lot of new complexity for the controller, in case we need, say two custom slugs in the URL (http://mylaravelapp.com/{slug}/another/{custom_slug}/{third_slug})

If the app will be the same, you can use server virtual hosts.
In Ubuntu + Nginx, you can use this recipe to guide you.
You can both make two virtual hosts or add, for example in Nginx, this kind of redirect:
server {
#implemented by default, change if you need different ip or port
#listen *:80 | *:8000;
server_name customdomain.com;
return 301 $scheme://mylaravelapp.com$request_uri;
}
Which will change:
http://customdomain.com/
http://customdomain.com/page
http://customdomain.com/another/{custom_slug}
To this, automatically:
http://mylaravelapp.com/
http://mylaravelapp.com/page
http://mylaravelapp.com/another/{custom_slug}
Another approach for Apache.

Related

yii2, how correctly cache data

Now i have code data like this:
my const
const CacheUserByUid = 'CacheUserByUid_';
const CacheUserByUsername = 'CacheUserByUsername_';
const CacheUserById = 'CacheUserByUsername_';
Get user data bu uid
/**
* Get user by uid , return user data for user profile
*
* #param $uid
* #return mixed
*/
public function getUserByUid($uid)
{
$result = Yii::$app->cache->getOrSet(self::CacheUserByUid . $uid, function () use ($uid) {
$result = self::find()
->select([
'id',
'username',
'email',
'city',
'country',
'name',
'avatar',
'about',
'uid',
])
->where(['uid' => trim($uid)])
->one();
if (!empty($result)) {
$result->id = (string)$result->id;
}
return $result;
});
return $result;
}
get user data by PK
/**
* #param $userId
* #return mixed
*/
public function getUserById($userId)
{
$user = Yii::$app->cache->getOrSet(self::CacheUserById . $userId, function () use ($userId) {
return self::findOne($userId);
});
return $user;
}
Get user by username
/**
* Get user by username. Return only for user front info
*
* #param $username
* #return array|\yii\db\ActiveRecord|null
*/
public function getUserByUsername($username)
{
$result = Yii::$app->cache->getOrSet(self::CacheUserByUsername . $username, function () use ($username) {
$result = self::find()->select([
'user.id',
'user.city',
'user.country',
'user.name',
'user.avatar',
'user.about',
'user.username'
])
->where(['username' => $username])
->one();
if (!empty($result)) {
$result->id = (string)$result->id;
}
});
return $result;
}
I cached this data. And where user was update i used:
/**
* #param $data
* #param $userId
* #return bool
* #throws \yii\db\Exception
*/
public function updateUser($data, $userId)
{
$user = $this->getUserById($userId);
if (!empty($user)) {
foreach ($data as $key => $name) {
if ($this->hasAttribute($key)) {
$user->$key = $name;
}
}
$user->updatedAt = time();
if ($user->save()) {
//чистимо кеш
FileCache::clearCacheByKey(self::CacheUserByUid . $user->uid);
FileCache::clearCacheByKey(self::CacheUserByUsername . $user->username);
FileCache::clearCacheByKey(self::CacheUserById . $user->id);
return true;
}
}
return false;
}
method clearCacheByKey
/**
* #param $key
*/
public static function clearCacheByKey($key)
{
if (Yii::$app->cache->exists($key)) {
Yii::$app->cache->delete($key);
}
}
Am I good at using a single-user cache that caches these requests in different keys? I don't see any other way out
Is it ok to cache user data in FileCache?
maybe it would be better to use something else for this?
In your case, such simple queries don't need to be cached explicitly. Yii already has a query cache and your requests definitely should be already stored in the cache. The key for data in cache would be a combination of your SQL's md5 with some connection metadata.
Just ensure that everything is configured correctly.
Also if you need to update cached data on some changes, make sure that you're making queries with the best for your case cache dependency. It can purge cached results by some auto condition or you can do it manually from your code(by using TagDependency)
What about FileCache it depends on traffic to your app and current infrastructure. Sometimes there is nothing criminal to store cache in files and you're always can switch to something like Redis/Memcache when your app grow big enough

laravel formrequest before middleware

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();
}
}

Yii2 $_GET and Yii::$app->request->get() not working

Background
I'm having an odd issue with a simple line of code that I don't understand. I have an Action on a Controller that i'm using for LinkedIn auth. The first time the user hits the controller it redirects to the LinkedIn site for authentication, once the user authenticates linked in redirects back to the same controller with the auth code in the url as a param.
http://beta.consynki.com/authentication/network/linkedin?code=DQTdGfxIlbsU...
Controller
class AuthenticationController extends WebController {
public function actionNetwork($network){
$access_token = Yii::$app->request->get('code');
$network_connection = NetworkFactory::build($network);
$client = $network_connection->getClient();
if($access_token && !is_null($access_token)){
$headers = Yii::$app->response->headers;
$headers->set('Pragma', 'no-cache');
$headers->add('X-Access-Token', $access_token);
return $this->render('success');
}
return $this->redirect($client->getLoginUrl(),302)->send();
}
}
EDIT 1 - WebController
/**
* Class WebController
*
* Default controller for public web pages. This class pulls meta tags from a seporately stored file, and makes
* them available to the view.
*
* #package www\components
*/
class WebController extends Controller {
public $meta = [];
public function beforeAction($event) {
$controller = $this->id;
$action = $this->action->id;
$meta_file = Yii::getAlias('#www') . '/meta/' . $controller . '/' . $action . '.php';
if (file_exists($meta_file)) {
$this->meta = include($meta_file);
$this->setMetaTags($this->meta);
}
return parent::beforeAction($event);
}
/**
* Set the meta tags for a page
*
* #param string $type
* #param $tag
* #param $value
*/
public function registerMetaTag($type = 'name', $tag, $value) {
if (!is_null($value)) {
$this->view->registerMetaTag([
$type => $tag,
'content' => $value
]);
}
}
public function behaviors() {
return [
/**
* The particular campaign used.
*
* Example social_share, stay_connected_add
*/
'utm_campaign' => [
'class' => 'common\behavior\TrackingBehavior',
'queryParam' => 'utm_campaign',
'sessionParam' => 'utm_campaign',
],
/*
* The source of the referral, could be an add network, facebook, email or just a link on the Consynki site.
*
* example: google, facebook, citysearch, welcome_email
*/
'utm_source' => [
'class' => 'common\behavior\TrackingBehavior',
'queryParam' => 'utm_source',
'sessionParam' => 'utm_source',
],
];
}
protected function setMetaTags($meta) {
foreach ($meta AS $key => $value) {
if (is_array($value)) {
$this->view->registerMetaTag($value, $key);
}
}
}
}
Problem
When I try to get the code from the GET param Yii::$app->request->get('code'); I get a NULL value. On further inspection of the $_GET array var_dump($app->request->get() or var_dump($_GET); I see the key for the code variable has a $ "?code" in front of it. This is very odd.
array(3) { ["network"]=> string(8) "linkedin" ["?code"]=> string(115) "DQTdGfxIlbsU..." ["state"]=> string(32) "12121..." }
Research Notes
It looks like Yii2 modify's the $_GET value as it passes the url routing. Not sure if this is the issue. Have updated to the latest version of yii and it didn't fix the problem.
Question
Why would this happen? How can I fix it so that I can get the code value?
Set rules there like this:
'authentication/network/<network:\w+>/<code:\w+>' => 'authentication/network',
'authentication/network/<network:\w+>' => 'authentication/network',
Now in action set parameters like:
public function actionNetwork($network, $code = null)
Your previous $access_token is now $code.

Fatal error: Class 'plugin' not found in 'path/to/file' - Wordpress plugin development

I'm developing a Wordpress plugin, and updating it through Wordpress' SVN. When entering the function to call back function getLogoUrl, I get this error:
Fatal error: Class 'plugin' not found in
/customers/1/6/2/carpe-noctem.no/httpd.www/wp-content/plugins/logo-switcher/inc/helpers.php
on line 21
The issue is that the class doesn't exist at all, and that the file is long gone deleted. The code in that file has been moved to a different file, yet the error shows up for a file that has been deleted from the system.
This is the file that has the class that should be called back.
<?php
/*
* This file is part of the logo-switcher package.
* (c) Iversen - Carpe Noctem <info#carpe-noctem.no>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
// Block direct access
if(!defined('ABSPATH'))exit;
/**
* Logo Switcher
*
* #author Iversen - Carpe Noctem
*/
if (class_exists('class_Logo_Switcher')) {
class class_Logo_Switcher
{
/**
* Add Theme Customize Support
*
* #param WP_Customize_Manager $manager
*/
public static function addThemeCustomizeSupport(\WP_Customize_Manager $manager)
{
// add the image filed
$manager->add_setting('logo_switcher');
$manager->add_control(new \WP_Customize_Image_Control($manager, 'logo_switcher', array(
'label' => __('Choose your logo', 'logo-switcher')
, 'section' => 'title_tagline'
, 'description' => __('Note: Depending on the current theme setting, the choosen logo might be used on the login page.', 'logo-switcher')
)));
}
/**
* Add the logo to the login page
*
* Change the logo in the login page and also change the url href and title
*
* #return boolean false if the optioh is disabled
*/
public static function addLoginSupport()
{
$setting = self::getOptions();
if (!$setting['enable-on-login-page'])
return false;
add_filter('login_headerurl', function() {
return get_bloginfo('url');
});
add_filter('login_headertitle', function() {
return get_bloginfo('description');
});
$url = static::getLogoUrl();
if (!empty($url)) {
list($width, $height, $type, $attr) = getimagesize($url);
print(
'<style type="text/css">'
. ".login h1 a {background-image: url('{$url}'); background-size: 100%; width:100%; height:{$height}px;}</style>"
);
} else {
print(
'<style type="text/css">.login h1 a {display:none}</style>'
);
}
}
/**
* Get options
*
* #return array
*/
public static function getOptions()
{
$default = array(
// path for default logo image
'default' => '/logo.png',
//the logo url (default to home page)
'url' => home_url('/'),
// the logo desciption default to (get_bloginfo('name', 'display'))
'description' => get_bloginfo('name', 'display'),
// enable logo display on the login page
'enable-on-login-page' => true,
);
return apply_filters('logo-switcher.options', $default);
}
/**
* Get the logo url
*
* #return string
*/
public static function getLogoUrl()
{
$setting = self::getOptions();
($result = get_theme_mod('logo_switcher')) && !empty($result) ?
$result : $setting['default'];
return esc_url($result);
}
/**
* Print logo url
*
* #param string $path the url target
* #param string $description the logo image description
*
*/
public static function printLogo($path = null, $description = null)
{
$setting = static::getOptions();
$path = !empty($path) ? $path : $setting['url'];
$description = !empty($description) ? $description : $setting['description'];
echo sprintf(
'<img src="%3$s" alt="%2$s">'
, esc_url($path)
, esc_attr($description)
, esc_url(static::getLogoUrl())
);
}
}
}
$Logo_Switcher_Plugin = new class_Logo_Switcher;
Any help to get?
Use not class_exists()
if (!class_exists('class_Logo_Switcher')) {
class class_Logo_Switcher { }
}

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