I need to check that {subcategory} has parent category {category}. How i can get the model of {category} in second binding?
I tried $route->getParameter('category');. Laravel throws FatalErrorException with message "Maximum function nesting level of '100' reached, aborting!".
Route::bind('category', function ($value) {
$category = Category::where('title', $value)->first();
if (!$category || $category->parent_id) {
App::abort(404);
}
return $category;
});
Route::bind('subcategory', function ($value, $route) {
if ($value) {
$category = Category::where('title', $value)->first();
if ($category) {
return $category;
}
App::abort(404);
}
});
Route::get('{category}/{subcategory?}', 'CategoriesController#get');
Update:
Now i made this, but i think it's not the best solution.
Route::bind('category', function ($value) {
$category = Category::where('title', $value)->whereNull('parent_id')->first();
if (!$category) {
App::abort(404);
}
Route::bind('subcategory', function ($value, $route) use ($category) {
if ($value) {
$subcategory = Category::where('title', $value)->where('parent_id', $category->id)->first();
if (!$subcategory) {
App::abort(404);
}
return $subcategory;
}
});
return $category;
});
You may try this and should work, code is self explanatory (ask if need an explanation):
Route::bind('category', function ($value) {
$category = Category::where('title', $value)->first();
if (!$category || $category->parent_id) App::abort(404);
return $category;
});
Route::bind('subcategory', function ($value, $route) {
$category = $route->parameter('category');
$subcategory = Category::where('title', $value)->whereParentId($category->id);
return $subcategory->first() ?: App::abort(404); // shortcut of if
});
Route::get('{category}/{subcategory?}', 'CategoriesController#get');
Update: (As OP claimed that, there is no parameter method available in Route class):
/**
* Get a given parameter from the route.
*
* #param string $name
* #param mixed $default
* #return string
*/
public function getParameter($name, $default = null)
{
return $this->parameter($name, $default);
}
/**
* Get a given parameter from the route.
*
* #param string $name
* #param mixed $default
* #return string
*/
public function parameter($name, $default = null)
{
return array_get($this->parameters(), $name) ?: $default;
}
I can't test this right now for you, but the closure function receives a $value and $route.
The last one is a instance of '\Illuminate\Routing\Route' (http://laravel.com/api/class-Illuminate.Routing.Route.html), and perhaps you could use the getParameter() method to retrieve some data....
I recently ran into same issues while trying to auto validate my stories existence inside my Session Model. So, i tried to check my Story model existence inside my Session Model using model bindings.
This is my solution
$router->bind('stories', function($value, $route) {
$routeParameters = $route->parameters();
$story = Story::where('id', $value)->where('poker_session_id', $routeParameters['poker_sessions']->id)->first();
if (!$story) {
throw new NotFoundHttpException;
}
return $story;
});
You can actually get route parameters using $route->parameters(), which returns an array. In my case, "poker_sessions" key contain an PokerSession Model, which i want.
Please be careful and use this only when you have a /category/{category}/subcategory/{subcategory} url like. Not subcategory without any {category}.
Good luck!.
Related
I am not sure, what is going on here. I have a collection of Model ID's but want to fallback on using all if specific ID's are omitted.
So I have this code:
use App\Models\Post;
function list($ids = [])
{
$posts = collect($ids)->whenEmpty(function ($posts) {
return Post::all()->pluck('id');
})->each(function ($item, $key) {
$post = Post::findOrFail($item);
});
}
Works fine if I pass in specific IDs via $ids. But when I leave it blank Post::all()->pluck('id'); inside of whenEmpty() returns empty. But if I call Post::all()->pluck('id'); outside the collection it works just fine. So I thought it might be some sort of scoping issue since its inside a closure, but changing it to:
use App\Models\Post;
function list($ids = [])
{
$posts = collect($ids)->whenEmpty(function ($posts) {
return \App\Models\Post::all()->pluck('id');
})->each(function ($item, $key) {
dd($item);
});
}
Is still showing up as "" If I dd() the whole collection its just:
[
0 => ""
]
So even providing the whole namespace isn't working. What am I missing here?
Here it's one approach more
function list(array $ids = [])
{
if(empty($ids)) $posts = Post::all();
else $posts = collect($ids)->map(function($id) {
return Post::findOrFail($id);
});
$posts->each(...); /* collection */
}
if you want to use whenEmpty()
function list(array $ids = [])
{
collect($ids)->map(function($id) {
return Post::findOrFail($id);
})->whenEmpty(function($posts){
return $posts = Post::all();
})->each(...);
}
I know this might not directly answer your question (because I would do this in a different way) but maybe it's helpful for you or others in the same situation.
What I would do is using a Request class to validate the introduced IDs like this:
use Illuminate\Validation\Rules\Exists;
class PostsRequests extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'ids' => ['array'],
'ids.*' => ['numeric', new Exists('posts','id')],
];
}
/**
* Handle a passed validation attempt.
*
* #return void
*/
public function passedValidation()
{
$this->ids = $this->validated()['ids'];
}
}
This way you make sure all the IDs introduced in the array exist in the posts table. Otherwise, the request fails.
Now that you know all the IDs are valid, you can simply check if the array is empty or not:
class PostController extends Controller
{
public function list(PostsRequests $request)
{
$posts = empty($request->ids) ? Post::all() : Post::whereIn('id', $request->ids])->get();
}
}
UPDATE
Since the array of IDs is not coming from a request, you can use a Validator in the controller itself:
use Illuminate\Validation\Rules\Exists;
use Illuminate\Support\Facades\Validator;
class PostController extends Controller
{
public function list(array $ids)
{
Validator::make($ids, [
'*' => ['numeric', new Exists('posts','id')],
])->validate();
$posts = empty($ids) ? Post::all() : Post::whereIn('id', $ids])->get();
}
}
I would like to create a more readable code by eliminating too many if statements but still does the job. I have tried creating a private method and extract the date range query and return the builder instance but whenever I do that, it does not return the correct builder query result so I end up smashing everything up on this method.
Other parameters will be added soon, so the if statements would pill up very fast. :(
Any tip on how to improve would be much appreciated. Thanks!
/**
* #param array $params
*
* #param $orderBy
* #param $sortBy
*
* #return Collection
*
* Sample:
* `/orders?release_date_start=2018-01-01&release_date_end=2018-02-20&firm_id=3` OR
* `/orders?claimId=3&status=completed`
*
* Problem: Too many if statements
*
*/
public function findOrdersBy(array $params, $orderBy = 'id', $sortBy = 'asc'): Collection
{
$release_date_start = array_get($params, 'release_date_start');
$release_date_end = array_get($params, 'release_date_end');
$claimId = array_get($params, 'claimId');
$firm_id = array_get($params, 'firm_id');
$status = array_get($params, 'status');
$orders = $this->model->newQuery();
if (!is_null($release_date_start) && !is_null($release_date_end)) {
$orders->whereBetween('releaseDate', [$release_date_start, $release_date_end]);
} else {
if (!is_null($release_date_start)) {
$orders->where('releaseDate', '>=', $release_date_start);
} else {
if (!is_null($release_date_end)) {
$orders->where('releaseDate', '<=', $release_date_end);
}
}
}
if (!is_null($claimId)) {
$orders->where(compact('claimId'));
}
if (!is_null($firm_id)) {
$orders->orWhere(compact('firm_id'));
}
if (!is_null($status)) {
$orders->where(compact('status'));
}
return $orders->orderBy($orderBy, $sortBy)->get();
}
if you are interested in using collection methods then you can use when() collection method to omit your if-else statements. So according to your statement it will look something like:
$orders->when(!is_null($release_date_start) && !is_null($release_date_end), function($q) {
$q->whereBetween('releaseDate', [$release_date_start, $release_date_end]);
}, function($q) {
$q->when(!is_null($release_date_start), function($q) {
$q->where('releaseDate', '>=', $release_date_start);
}, function($q) {
$q->when(!is_null($release_date_end), function($q) {
$q->where('releaseDate', '<=', $release_date_end);
})
})
})
->when(!is_null($claimId), function($q) {
$q->where(compact('claimId'));
})
->when(!is_null($firm_id), function($q) {
$q->orWhere(compact('firm_id'));
})
->when(!is_null($status), function($q) {
$q->where(compact('status'));
})
For more information you can see conditional-clauses in documentation. Hope this helps.
One option you can use is ternary operation in php like this:
$claimId ? $orders->where(compact('claimId')) : ;
$firm_id ? $orders->orWhere(compact('firm_id')) : ;
$status ? $orders->where(compact('status')) : ;
It would be cleaner than is statements code.
Another option you can use in laravel is Conditional Clauses
Thanks for your suggestions but I came up with another solution:
/**
* #param array $params
*
* #param $orderBy
* #param $sortBy
*
* #return Collection
*/
public function findOrdersBy(array $params, $orderBy = 'id', $sortBy = 'asc'): Collection
{
$release_date_start = array_get($params, 'release_date_start');
$release_date_end = array_get($params, 'release_date_end');
$orders = $this->model->newQuery();
if (!is_null($release_date_start) && !is_null($release_date_end)) {
$orders->whereBetween('releaseDate', [$release_date_start, $release_date_end]);
} else {
if (!is_null($release_date_start)) {
$orders->where('releaseDate', '>=', $release_date_start);
} else {
if (!is_null($release_date_end)) {
$orders->where('releaseDate', '<=', $release_date_end);
}
}
}
$fields = collect($params)->except($this->filtersArray())->all();
$orders = $this->includeQuery($orders, $fields);
return $orders->orderBy($orderBy, $sortBy)->get();
}
/**
* #param Builder $orderBuilder
* #param array $params
*
* #return Builder
*/
private function includeQuery(Builder $orderBuilder, ... $params) : Builder
{
$orders = [];
foreach ($params as $param) {
$orders = $orderBuilder->where($param);
}
return $orders;
}
/**
* #return array
*/
private function filtersArray() : array
{
return [
'release_date_start',
'release_date_end',
'order_by',
'sort_by',
'includes'
];
}
The main factor on the private method includeQuery(Builder $orderBuilder, ... $params) which takes $params as variable length argument. We just iterate the variables being passed as a query parameter /orders?code=123&something=test and pass those as a where() clause in the query builder.
Some parameters may not be a property of your object so we have to filter only the query params that match the object properties. So I created a filtersArray() that would return the parameters to be excluded and prevent an error.
Hmmm, actually, while writing this, I should have the opposite which is only() otherwise it will have an infinite of things to exclude. :) That would be another refactor I guess. :P
I have a customer request as follows:
<textarea name="intro[en]"></textarea>
<textarea name="intro[fr]"></textarea>
<textarea name="intro[de]"></textarea>
I am validating it with a custom request:
class UpdateProfileRequest extends Request
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'intro.*' => 'required|max:100'
];
}
}
The validator is not working. I think this is because the .* only works for numbered arrays, rather than associative arrays?
I'm not sure how to go about doing this.
Is there a way to do it with a custom request like this? If so what is the syntax?
Otherwise, what should I do. I already wrote some custom code inside the controller method like this:
$hasIntro = false;
$hasBio = false;
foreach($request->get('intro') as $language => $localIntro)
{
if(!empty($request->get('intro')[$language]))
{
$hasIntro = true;
}
}
if(!$hasIntro or !$hasBio)
{
return redirect()->back()->withErrors('You must enter at least 1 Bio and 1 Intro');
}
Which I think might be one manual way of going about this. Though I believe withErrors requires a validator, so I'm back to the same problem... Though perhaps there is a way to do this manually?
My ideal solution is to find the associative array syntax, if that indeed exists?
I'm not sure about the right way
but my idea is something like this
public function rules($inputs)
{
$rules = [];
foreach ($inputs as $key => $val) {
if ( strpos($key, "intro") === 0 ){
$rules[$key] = 'required|max:100';
}
}
return $rules;
}
class UpdateProfileRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'intro.*' => 'required|max:100'
];
}
/**
* #param Validator $validator
*
* #return mixed
*/
protected function formatErrors(Validator $validator)
{
return $validator->errors()->all();
}
}
You have below same name so make sure it's different or remove one, change name.
<textarea name="intro[fr]"></textarea>
<textarea name="intro[fr]"></textarea>
public function rules()
{
$rules = [];
$intro = $this->request->get('intro');
if (!empty($intro)) {
foreach ($intro as $index => $doc) {
$rules[sprintf('intro.%d', $index)] = 'required|max:100';
}
}
return $rules;
}
I am using October CMS built on Laravel, and I am having some strange issues I am not sure how to interpret.
Code Sample 1: Works fine (Component EstateList)
public function onRun()
{
$this->listEstates();
}
/**
* Pulls all the estates from the model
*
* #return $estateList
*/
protected function listEstates()
{
$estateList = RealEstate::all();
return $estateList;
}
Code Sample 2: Doesn't work (Component EstateDetails)
public function onRun()
{
$this->show();
}
/**
* Returns the slug and display individual Estate Object
*
* #return $pageDetails
*/
protected function show()
{
$slug = $this->param('slug');
$pageDetails = RealEstate::find($slug);
echo $slug; //returns slug as it should
echo $pageDetails; //empty
if ($pageDetails) {
return $pageDetails;
} else {
return \Response::make('Page not found', 404);
}
}
If I just put the code of show() into the function onRun() it works fine. Why does echo echo $pageDetails return empty on the Code Sample 2? if it is ran in a seperate function show().
Thank you for your help.
Try changing it to RealEstate::where('slug', '=', $slug)->firstOrFail();. The find bit searches the ID table for the column, not the slug.
You are not 'returning' the Response from show()
Try changing $this->show(); to return return $this->show(); in onRun()
Try changing your code to this
public function onRun()
{
return $this->show();
}
/**
* Returns the slug and display individual Estate Object
*
* #return $pageDetails
*/
protected function show()
{
$slug = $this->param('slug');
$pageDetails = RealEstate::where('slug', '=', $slug)->firstOrFail();;
echo $slug; //returns slug as it should
echo $pageDetails; //empty
if ($pageDetails) {
return $pageDetails;
} else {
return \Response::make('Page not found', 404);
}
}
Hopefully This will solve your problem
I'm trying to include unit-testing in a new Laravel app I'm building.
Right now I'm want to test my OrderController. The index method of this controller looks like this:
public function index()
{
// We need the extra 'orders()' for the query scope
$orders = $this->order->orders()->paginate($this->perPage);
$this->layout->content = View::make('orders.index', compact('orders'));
}
Now I have my test that looks like this:
public function testIndex()
{
$this->call('GET', 'orders');
$this->assertResponseOk();
$this->assertViewHas('orders');
}
Now if I run phpunit, the test does run, but I'm getting: 1) OrderControllerTest::testIndex
Failed asserting that an array has the key 'orders'.
I've tracked down the issue to using Controller Layouts $this->layout.
If I just do return View::make() the test does pass, if I return $this->layout... it also does pass, but this destroys the actual app.
So only option I've found is to use return View::make() and have #extends('master') in that view. But it's seems strange to me that you can't use Controller Layouts in your app if you want to test it.
Am I doing something wrong?
Edit:
I had the same problem and here is slightly messy solution that works!
View::composer('ordersviewname', function($view) {
$this->assertArrayHasKey('orders', $view->getData());
});
$this->call('GET', 'orders');
Note that you have to put this code BEFORE $this->call()
EDIT:
Here is more elegant solution!
Add functions to TestCase
protected $nestedViewData = array();
public function registerNestedView($view)
{
View::composer($view, function($view){
$this->nestedViewsData[$view->getName()] = $view->getData();
});
}
/**
* Assert that the given view has a given piece of bound data.
*
* #param string|array $key
* #param mixed $value
* #return void
*/
public function assertNestedViewHas($view, $key, $value = null)
{
if (is_array($key)) return $this->assertNestedViewHasAll($view, $key);
if ( ! isset($this->nestedViewsData[$view]))
{
return $this->assertTrue(false, 'The view was not called.');
}
$data = $this->nestedViewsData[$view];
if (is_null($value))
{
$this->assertArrayHasKey($key, $data);
}
else
{
if(isset($data[$key]))
$this->assertEquals($value, $data[$key]);
else
return $this->assertTrue(false, 'The View has no bound data with this key.');
}
}
/**
* Assert that the view has a given list of bound data.
*
* #param array $bindings
* #return void
*/
public function assertNestedViewHasAll($view, array $bindings)
{
foreach ($bindings as $key => $value)
{
if (is_int($key))
{
$this->assertNestedViewHas($view, $value);
}
else
{
$this->assertNestedViewHas($view, $key, $value);
}
}
}
public function assertNestedView($view)
{
$this->assertArrayHasKey($view, $this->nestedViewsData);
}
Now you would rewrite your test
$view='orders';
$this->registerNestedView($view);
$this->call('GET', 'orders');
$this->assertNestedViewHas($view, 'orders');
assertNestedViewHas() has all the same feautures as assertViewHas() !
I added another function assertNestedView() which just check if given view was called.