Laravel Scout and TNTSearch search withTrashed - laravel

I've configured Laravel Scout and can use ::search() on my models.
The same models also use SoftDeletes. How can I combine the ::search() with withTrashed()?
The code below does not work.
MyModel::search($request->input('search'))->withTrashed()->paginate(10);
The below does work but does not include the trashed items.
MyModel::search($request->input('search'))->paginate(10);
Update 1
I found in the scout/ModelObserver that deleted items are made unsearchable. Which is a bummer; I wanted my users to be able to search through their trash.
Update 2
I tried using ::withoutSyncingToSearch, as suggested by #camelCase, which I had high hopes for, but this also didn't work.
$model = MyModel::withTrashed()->where('slug', $slug)->firstOrFail();
if ($model->deleted_at) {
$model->forceDelete();
} else {
MyModel::withoutSyncingToSearch(function () use ($model) {
$model->delete();
});
}
This caused an undefined offset when searching for a deleted item. By the way, I'm using the TNTSearch driver for Laravel Scout. I don't know if this is an error with TNTSearch or with Laravel Scout.

I've worked out a solution to your issue. I'll be submitting a pull request for Scout to hopefully get it merged with the official package.
This approach allows you to include soft deleted models in your search:
App\User::search('search query string')->withTrashed()->get();
To only show soft deleted models in your search:
App\User::search('search query string')->onlyTrashed()->get();
You need to modify 3 files:
Builder.php
In laravel\scout\src\Builder.php add the following:
/**
* Whether the search should include soft deleted models.
*
* #var boolean
*/
public $withTrashed = false;
/**
* Whether the search should only include soft deleted models.
*
* #var boolean
*/
public $onlyTrashed = false;
/**
* Specify the search should include soft deletes
*
* #return $this
*/
public function withTrashed()
{
$this->withTrashed = true;
return $this;
}
/**
* Specify the search should only include soft deletes
*
* #return $this
*/
public function onlyTrashed()
{
$this->onlyTrashed = true;
return $this;
}
/**
* Paginate the given query into a simple paginator.
*
* #param int $perPage
* #param string $pageName
* #param int|null $page
* #return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginate($perPage = null, $pageName = 'page', $page = null)
{
$engine = $this->engine();
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$perPage = $perPage ?: $this->model->getPerPage();
$results = Collection::make($engine->map(
$rawResults = $engine->paginate($this, $perPage, $page), $this->model, $this->withTrashed, $this->onlyTrashed
)); // $this->withTrashed, $this->onlyTrashed is new
$paginator = (new LengthAwarePaginator($results, $engine->getTotalCount($rawResults), $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]));
return $paginator->appends('query', $this->query);
}
Engine.php
In laravel\scout\src\Engines\Engine.php modify the following:
/**
* Map the given results to instances of the given model.
*
* #param mixed $results
* #param \Illuminate\Database\Eloquent\Model $model
* #param boolean $withTrashed // New
* #return \Illuminate\Database\Eloquent\Collection
*/
abstract public function map($results, $model, $withTrashed, $onlyTrashed); // $withTrashed, $onlyTrashed is new
/**
* Get the results of the given query mapped onto models.
*
* #param \Laravel\Scout\Builder $builder
* #return \Illuminate\Database\Eloquent\Collection
*/
public function get(Builder $builder)
{
return Collection::make($this->map(
$this->search($builder), $builder->model, $builder->withTrashed, $builder->onlyTrashed // $builder->withTrashed, $builder->onlyTrashed is new
));
}
And finally, you just need to modify your relative search engine. I'm using Algolia, but the map method appears to be the same for TNTSearch.
AlgoliaEngine.php
In laravel\scout\src\Engines\AlgoliaEngine.php modify the map method to match the abstract class we modified above:
/**
* Map the given results to instances of the given model.
*
* #param mixed $results
* #param \Illuminate\Database\Eloquent\Model $model
* #param boolean $withTrashed // New
* #return \Illuminate\Database\Eloquent\Collection
*/
public function map($results, $model, $withTrashed, $onlyTrashed) // $withTrashed, $onlyTrashed is new
{
if (count($results['hits']) === 0) {
return Collection::make();
}
$keys = collect($results['hits'])
->pluck('objectID')->values()->all();
$modelQuery = $model->whereIn(
$model->getQualifiedKeyName(), $keys
);
if ($withTrashed) $modelQuery->withTrashed(); // This is where the query will include deleted items, if specified
if ($onlyTrashed) $modelQuery->onlyTrashed(); // This is where the query will only include deleted items, if specified
$models = $modelQuery->get()->keyBy($model->getKeyName());
return Collection::make($results['hits'])->map(function ($hit) use ($model, $models) {
$key = $hit['objectID'];
if (isset($models[$key])) {
return $models[$key];
}
})->filter();
}
TNTSearchEngine.php
/**
* Map the given results to instances of the given model.
*
* #param mixed $results
* #param \Illuminate\Database\Eloquent\Model $model
*
* #return Collection
*/
public function map($results, $model, $withTrashed, $onlyTrashed)
{
if (count($results['ids']) === 0) {
return Collection::make();
}
$keys = collect($results['ids'])->values()->all();
$model_query = $model->whereIn(
$model->getQualifiedKeyName(), $keys
);
if ($withTrashed) $model_query->withTrashed();
if ($onlyTrashed) $model_query->onlyTrashed();
$models = $model_query->get()->keyBy($model->getKeyName());
return collect($results['ids'])->map(function ($hit) use ($models) {
if (isset($models[$hit])) {
return $models[$hit];
}
})->filter();
}
Let me know how it works.
NOTE: This approach still requires you to manually pause syncing using the withoutSyncingToSearch method while deleting a model; otherwise the search criteria will be updated with unsearchable().

this is my solution.
// will return matched ids of my model instance
$searchResultIds = MyModel::search($request->input('search'))->raw()['ids'];
MyModel::whereId('id', $searchResultIds)->onlyTrashed()->get();

Related

Laravel and repository/gateway patterns. Did I lose the fillable property for future helper?

I'm following this answer with his pattern:
How to make a REST API first web application in Laravel
At the end my application works with these methods:
LanController
/**
*
* Store the data in database
*
* #param Request $request
* #return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|void
*/
public function store(Request $request)
{
$status = $this->lan_gateway->create($request->all());
/**
* Status is true if insert in database is OK
* or an array of errors
*/
if ($status===true) {
return redirect(route('lan'))
->with('success',trans('common.operation_completed_successfully'));
// validation ok
} else {
return redirect(route('lan-create'))
->withInput()
->withErrors($status);
// validation fails
}
}
LanGateway
/**
* Validate input and create the record into database
*
* #param array $data the values to insert
* #return array $validator errors on fails
* #return bool $status true on success
*/
public function create(array $data)
{
$validator = Validator::make($data, [
'ip_address' => 'required|string'
]);
if ($validator->fails()) {
return $validator->errors()->all();
} else {
$status = $this->lan_interface->createRecord($data);
return $status;
}
}
And this is the interface implemented by repository create method
<?php
/**
* Repository for LAN object.
* PRG paradigma, instead of "User"-like class Model
*
* #see https://stackoverflow.com/questions/23115291/how-to-make-a-rest-api-first-web-application-in-laravel
*/
namespace App\Repositories;
use App\Interfaces\LanInterface;
use Illuminate\Database\Eloquent\Model;
class LanRepository extends Model implements LanInterface
{
/**
* The name of table on database
* #var string The table name on database
*/
protected $table = "lans";
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['ip_address'];
public function getAllRecords()
{
$lan = $this->all();
return $lan;
}
/**
*
* Insert record inside database.
*
* #param array $data
* #return bool true on success, false on failure
*/
public function createRecord(array $data)
{
foreach ($data as $key => $value) {
if (in_array($key,$this->fillable)) {
$this->$key = $value;
}
}
$status = $this->save();
return $status;
}
You can see that I "lost" the fillable helper methods, at the end I use only as "needle/haystack".
Is there another implementation possible for my createRecord method to use, as much as possible, Laravel default methods/helpers or am I in the most right direction?
Thank you very much
As you can see in documentation, $fillable property only applies when you are using mass assignment statements.
Now if if (in_array($key,$this->fillable)) { conditional check is just to check if only some allowed set of columns are saved from API, you can create another property lets say protected $allowedToInsert = ['column1', 'column2],..; and then update the conditional :
public function createRecord(array $data)
{
foreach ($data as $key => $value) {
if (in_array($key,$this->allowedToInsert)) {
$this->$key = $value;
}
}
$status = $this->save();
return $status;
}
This way you can use fillable as it is supposed to be used, The approach you have can be the simplest one but you can improve lit of things. See this for your reference

October CMS: share partial between themes

What is the simple way to share partials between themes. Assets are possible but i am having issue with whole partials. I have a multi-theme site managed/activated by sub-domain policies/logic.
SOLUTIONS
/**
*
* Renders a requested partial in context of this component,
* see Cms\Classes\Controller#renderPartial for usage.
*/
/**
* #param $themeName
* #param $partialName
* #param $data
* #return mixed
* #throws \Cms\Classes\CmsException
*/
public function renderThemePartial($partialName, $themeName, $data)
{
$theme = Theme::getActiveTheme();
if($themeName) {
$theme = Theme::load($themeName);
}
$controller = new Controller($theme);
return $controller->renderPartial($partialName, $data);
}
/**
*
* Renders a requested content in context of this component,
* see Cms\Classes\Controller#renderContent for usage.
*/
/**
* #param $themeName
* #param $contentName
* #param $data
* #return string
* #throws \Cms\Classes\CmsException
*/
public function renderThemeContent($contentName, $themeName, $data)
{
$theme = Theme::getActiveTheme();
if($themeName) {
$theme = Theme::load($themeName);
}
$controller = new Controller($theme);
return $controller->renderContent($contentName, $data);
}
public function registerMarkupTags()
{
return [
'functions' => [
'partial_from_theme' => [$this, 'themePartial'],
'content_from_theme' => [$this, 'themeContent'],
],
'filters' => [
'theme_asset' => [$this, 'themeUrl']
]
];
}
/**
* #param $requested
* #return string
*/
public function themeUrl($requested)
{
$asset = $requested[0];
$theme = $requested[1];
$theme = Theme::load($theme);
$themeDir = $theme->getDirName();
if (is_array($asset)) {
$_url = Url::to(CombineAssets::combine($asset, themes_path().'/'.$themeDir));
}
else {
$_url = Config::get('cms.themesPath', '/themes').'/'.$themeDir;
if ($asset !== null) {
$_url .= '/'.$asset;
}
$_url = Url::asset($_url);
}
return $_url;
}
/**
* #param $partialName
* #param null $themeName
* #param array $parameters
* #return mixed
* #throws \Cms\Classes\CmsException
*/
public function themePartial($partialName, $themeName = null, $parameters = [])
{
return $this->renderThemePartial($partialName, $themeName, $parameters);
}
/**
* #param $contentName
* #param null $themeName
* #param array $parameters
* #return string
* #throws \Cms\Classes\CmsException
*/
public function themeContent($contentName, $themeName = null, $parameters = [])
{
return $this->renderThemeContent($contentName, $themeName, $parameters);
}
I guess you can use Plugin component here you can add component to page and use its partial.
You need to define partial [ you can add as many as you like ] in component.
And just use that partial like below in page , now this same thing you can do in other themes as well.
When you change partial it will affect in all other themes like its shared across multiple themes.
Down side will be that you can not change its content from backend, you need to manually change that file from ftp or other manner.
Partial from another theme
Hmm
you can add custom twig function for that may be
In any of your plugin you can add this code
public function registerMarkupTags()
{
return [
'functions' => [
'theme_partial' => [$this, 'themePartial'],
]
];
}
public function themePartial($partialName, $themeName = null, $vars = [])
{
$theme = Theme::getActiveTheme();
if($themeName) {
$theme = Theme::load($themeName);
}
$template = call_user_func([Partial::class, 'load'], $theme, $partialName);
$partialContent = $template->getTwigContent();
$html = Twig::parse($partialContent, $vars);
return $html;
}
Now inside your theme when you want to use partial you can use
{{ theme_partial('call_from_other_theme', 'stemalo' ,{'name':'hardik'}) }}
^ Your theme name here.
themes/stemalo/partials/call_from_other_theme.htm content
description = "Shared Partial."
[viewBag]
==
<div id="shared_partial">
<h1>Another Theme Partial : {{name}}</h1>
</div>
this way you can use partial from another themes.
you aksed for access to theme directory here may be you can just you need to pass theme name, and `its dynamic you can pass variable as theme name so it can pick any thing you like.
If any doubt please comment.

TYPO3 7.6 - Add a public function to the controller

I just try to create my first extension about flowers with a list view and and a detail view. Now I want to add the possibility to browse through the flowers on detail view.
I found the following code Extbase Repository: findNext und findPrevious Funktionen
and added it to my repository
/**
* The repository for Pflanzens
*/
class PflanzenRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
protected $defaultOrderings = array(
'nameDeutsch' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING
);
/**
* Find next item by uid
* #param integer $uid The uid of the current record
* #return boolean|\TYPO3\CMS\Extbase\Persistence\Generic\QueryResult
*/
public function findNext($uid) {
$query = $this->createQuery();
$result = $query->matching($query->greaterThan('uid',$uid))->setLimit(1)->execute();
if($query->count()) {
return $result;
} else {
return false;
}
}
/**
* Find previous item by uid
* #param integer $uid The uid of the current record
* #return boolean|\TYPO3\CMS\Extbase\Persistence\Generic\QueryResult
*/
public function findPrev($uid) {
$query = $this->createQuery();
$ordering = array('uid'=>\TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING);
$result = $query->matching($query->lessThan('uid',$uid))->setLimit(1)->setOrderings($ordering)->execute();
if($query->count()) {
return $result;
} else {
return false;
}
}
}
This is my controller right now:
/**
* PflanzenController
*/
class PflanzenController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController
{
/**
* pflanzenRepository
*
* #var \TMRuebe\Faerbepflanzen\Domain\Repository\PflanzenRepository
* #inject
*/
protected $pflanzenRepository = NULL;
/**
* action list
*
* #return void
*/
public function listAction()
{
$pflanzens = $this->pflanzenRepository->findAll();
$this->view->assign('pflanzens', $pflanzens);
}
/**
* action show
*
* #param \TMRuebe\Faerbepflanzen\Domain\Model\Pflanzen $pflanzen
* #return void
*/
public function showAction(\TMRuebe\Faerbepflanzen\Domain\Model\Pflanzen $pflanzen)
{
$this->view->assign('pflanzen', $pflanzen);
}
}
Now I need help how to add the two public functions to the controller. And I also need a hint for the variable that I can use in my fluid template to create the previous link and the next link.
in showAction() you need to assign to further variables with the results of findNext() and findPrev().
$this->view->assign('previous', \TMRuebe\Faerbepflanzen\Domain\Repository\PflanzenRepository::findPrev($pflanzen['uid']));
$this->view->assign('next', \TMRuebe\Faerbepflanzen\Domain\Repository\PflanzenRepository::findNext($pflanzen['uid']));
in your detail template you need to build the links like the links in the list view.
You might build methods using the current object to get easier access to next and prev.

Doctrine Event Listener for adding/removing Many to Many relations

I make heavy use of Entity Listeners for logging purposes, generally works really well and keeps all the code out of the controllers/services.
One thing I haven't been able to achieve is logging of items added to a ManyToMany relation. In this instance I want to log when a size is added/removed from a product
/**
* #ORM\Entity
* #ORM\EntityListeners({"EventListener\ProductListener"})
* #ORM\Table(name="products")
*/
class Product
{
// ...
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Size")
* #ORM\JoinTable(name="productSizes",
* joinColumns={#ORM\JoinColumn(name="productId", referencedColumnName="productId")},
* inverseJoinColumns={#ORM\JoinColumn(name="sizeId", referencedColumnName="sizeId")}
* )
*/
protected $sizes;
/**
* #param Size $size
* #return Product
*/
public function addSize(Size $size)
{
$this->sizes[] = $size;
return $this;
}
/**
* #param Size $size
*/
public function removeSize(Size $size)
{
$this->sizes->removeElement($size);
}
/**
* #return ArrayCollection
*/
public function getSizes()
{
return $this->sizes;
}
// ...
}
Then inside the entity listener
class ProductListener
{
// ...
/**
* #ORM\PostPersist
*/
public function postPersistHandler(Product $product, LifecycleEventArgs $args)
{
$this->getLogger()->info("Created product {$product->getSku()}", [
'productId' => $product->getId()
]);
}
/**
* #ORM\PostUpdate
*/
public function postUpdateHandler(Product $product, LifecycleEventArgs $args)
{
$context = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($product);
$context['productId'] = $product->getId();
$this->getLogger()->info("Updated product", $context);
}
// ...
}
So how can I get the colours added/removed from the unit of work? I'm assuming this is available somewhere but I can't find it.
In your ProductListener
$product->getSizes() returns instance of Doctrine\ORMPersistentCollection. Then you can call 2 methods:
- getDeleteDiff - returns removed items
- getInsertDiff - returns added items

Can't make a new Insertion - Laravel Eloquent ORM

I can't Insert into this table and this drives me crazy
This is the error Msg I get
var_export does not handle circular references
open: /var/www/frameworks/Scout/vendor/laravel/framework/src/Illuminate/Database/Connection.php
* #param Exception $e
* #param string $query
* #param array $bindings
* #return void
*/
protected function handleQueryException(\Exception $e, $query, $bindings)
{
$bindings = var_export($bindings, true);
$message = $e->getMessage()." (SQL: {$query}) (Bindings: {$bindings})";
Here is my Full Mode
<?php
namespace Models;
use Illuminate\Database\Eloquent\Collection;
class Student extends \Eloquent
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'students';
/**
* The rules used to validate new Entry.
*
* #var array
*/
protected $newValidationRules = array(
'studentCode' => 'unique:students,code|numeric|required',
'studentName' => 'required|min:2',
'dateOfBirth' => 'date',
'mobile' => 'numeric'
);
/**
* Relation with sessions (Many To Many Relation)
* We added with Created_at to the Pivot table as it indicates the attendance time
*/
public function sessions()
{
return $this->belongsToMany('Models\Session', 'student_session')->withPivot('created_at')->orderBy('created_at', 'ASC');
}
/**
* Get Student Subjects depending on attendance,
*/
public function subjects()
{
$sessions = $this->sessions()->groupBy('subject_id')->get();
$subjects = new Collection();
foreach ($sessions as $session) {
$subject = $session->subject;
$subject->setRelation('student', $this);
$subjects->add($subject);
}
return $subjects;
}
/**
* Insert New Subject
* #return Boolean
*/
public function insertNew()
{
$this->validator = \Validator::make(\Input::all(), $this->newValidationRules);
if ($this->validator->passes()) {
$this->name = \Input::get('studentName');
$this->code = \Input::get('studentCode');
if ($this->save()) {
return \Response::make("You have registered the subject successfully !");
} else {
return \Response::make('An Error happened ');
}
} else {
Return $this->validator->messages()->first();
}
}
}
I am just trying to insert a new row with three Columns (I call the insertNew function on instance of Student)
1- ID automatically incremented
2- Special Code
3- Name
And I got this above Msg
What's I have tried till now :
removing all relations between from this model and other models
that has this one in the relation
Removed the validation step in insertNew()
Removed the all Input class calls and used literal data instead.
note that I use similar Inserting function on other Models and it works flawlessly
Any Comments , Replies are appreciated :D
Solution
I solved it and the problem was that I am accessing the validator
$this->validator = \Validator::make(\Input::all(), $this->newValidationRules);
And it was because I forgot that
/**
* The validator object.
*
* #var Illuminate\Validation\Validator
*/
protected $validator;
I had a similar problem. But to me, changing this code:
if ($this->validator->passes()) {
$this->name = \Input::get('studentName');
$this->code = \Input::get('studentCode');"
to this:
if ($this->validator->passes()) {
$this->setAttribute ("name" , \Input::get('studentName'));
$this->setAttribute ("code" , \Input::get('studentCode'));"
solved it.

Resources