I'm sorting table columns in Joomla Backend. I adjust settings according to this tutorial.
As we can see it is suggested to override populateState method and manually obtain sorting options.
public function populateState() {
$filter_order = JRequest::getCmd('filter_order');
$filter_order_Dir = JRequest::getCmd('filter_order_Dir');
$this->setState('filter_order', $filter_order);
$this->setState('filter_order_Dir', $filter_order_Dir);
}
But I noticed that the native component com_content does not set these options explicitly in the model file administrator/components/com_content/models/articles.php.
protected function populateState($ordering = null, $direction = null)
{
// Initialise variables.
$app = JFactory::getApplication();
$session = JFactory::getSession();
............................................
............................................
............................................
// List state information.
parent::populateState('a.title', 'asc');
}
Instead it just invokes parent populateState. And in fact JModelList::populateState() includes this:
protected function populateState($ordering = null, $direction = null)
{
// If the context is set, assume that stateful lists are used.
if ($this->context) {
$app = JFactory::getApplication();
.....................................
.....................................
.....................................
$value = $app->getUserStateFromRequest($this->context.'.ordercol', 'filter_order', $ordering);
if (!in_array($value, $this->filter_fields)) {
$value = $ordering;
$app->setUserState($this->context.'.ordercol', $value);
}
$this->setState('list.ordering', $value);
// Check if the ordering direction is valid, otherwise use the incoming value.
$value = $app->getUserStateFromRequest($this->context.'.orderdirn', 'filter_order_Dir', $direction);
if (!in_array(strtoupper($value), array('ASC', 'DESC', ''))) {
$value = $direction;
$app->setUserState($this->context.'.orderdirn', $value);
}
$this->setState('list.direction', $value);
}
else {
$this->setState('list.start', 0);
$this->state->set('list.limit', 0);
}
}
So I'm trying to imitate the code of the native com_content. Thus I assume that
class CompViewData extends JView
{
function display($tpl = null)
{
$this->state = $this->get('State');
Will invoke parent JModelList::populateState() (so I'm not overriding it in the modal class) and set $this->setState('list.ordering', $value);. But for some reason when I invoke $this->state->get() in getListQuery() to build my SQL query with ordering
protected function getListQuery()
{
$orderCol = $this->state->get('list.ordering', 'id');
$orderDirn = $this->state->get('list.direction', 'asc');
This variables happen to be not defined.
What am I missing? I assume it is somehow connected with proper user session, but I don't have evidence whatsoever.
After just coming across the same issue I found that, as you said, the superclass populateState() does indeed have the behaviour defined. However, it also does a check to ensure your field is in the "whitelist".
if (!in_array($value, $this->filter_fields))
If you look at com_content you will see this section at the top of the model class (in your case models/articles.php):
public function __construct($config = array())
{
if (empty($config['filter_fields']))
{
$config['filter_fields'] = array(
'id', 'a.id',
'title', 'a.title',
//...(more fields here)
'publish_up', 'a.publish_up',
'publish_down', 'a.publish_down',
);
$app = JFactory::getApplication();
$assoc = isset($app->item_associations) ? $app->item_associations : 0;
if ($assoc)
{
$config['filter_fields'][] = 'association';
}
}
parent::__construct($config);
}
You will need to include this section so that the ModelList class knows that the 'ordering' field is in the whitelist. Obviously substitute the fields with those on which you wish to filter.
The Joomla JModelList defines populateState like this
protected function populateState($ordering = null, $direction = null)
It means that if you do not have populateState override in you class, this will be called but it gets no values. The minimum requirement is to set default values if you want to use ordering. You may completely delete this method from your class if you are not planning to use ordering at all.
So, minimum what you need is to interpolate in your class
protected function populateState($ordering = null, $direction = null) {
parent::populateState('id', 'ACS');
}
Otherwise you will not get anything in $state->get() or $this->state->get() unless you click on ordering column. Then parent's populateState will take variables from request.
Related
I am trying to implement a widgets library using load->view. I know I can use include to call directly the file and avoid the vars cache issues but just wondering why it does not work.
Here is how I have structured my code:
My Controller:
class Page extends MY_Controller {
public $data = array();
public function __construct() {
parent::__construct();
...
$this->load->library('widgetmanager');
}
public function index($slug = '') {
echo $this->widgetmanager->show(2);
echo $this->widgetmanager->show(1);
}
}
My Library
class WidgetManager
{
private $CI;
public function __construct()
{
$this->CI = & get_instance();
}
public function show($widget_id) {
$data = array();
$widget_id = (int)$widget_id;
$this->CI->db->select('*');
$this->CI->db->from('widget');
$this->CI->db->where('id', $widget_id);
$query = $this->CI->db->get();
$item = $query->row_array();
$data['widget_title'] = $item['title'];
$data['widget_content'] = $item['content'];
$widget = $this->CI->load->view('widget/'.$item['source'], $data, TRUE);
$data['widget_title'] = '';
$data['widget_content'] = '';
$this->CI->load->view('widget/'.$item['source'], $data);
return $widget;
}
}
widget 1: Calls widget/content
widget 2: Calls widget/banner
What is happening is, the vars set on the first widget call (they are same name as second widget call), get cached, meaning values from the first call are passed to same call. It is weird because are different views.
I have tried:
Using clear_vars(): $this->CI->load->clear_vars(), before and after doing load->view on the library.
Calling load->view with empty array, null, etc
Tried to add a prefix with the widget slug to the vars (that works, but I have to send in some way the prefix to the view, so it is not possible due cache issue)
Any ideas?
Here is what should work.
(I took the liberty of simplifying your database call making it require much less processing.)
public function show($widget_id)
{
$data = array();
$widget_id = (int) $widget_id;
$item = $this->CI->db
->get_where('widget', array('id' => $widget_id))
->row_array();
$data['widget_title'] = $item['title'];
$data['widget_content'] = $item['content'];
$widget = $this->CI->load->view('widget/'.$item['source'], $data, TRUE);
//clear the cached variables so the next call to 'show()' is clean
$this->CI->load->clear_vars();
return $widget;
}
On further consideration The call $this->CI->load->clear_vars(); is probably pointless because each time WidgetManager::show() is called the $data var is recreated with exactly the same keys. When the $data var is passed to load->view the new values of $data['widget_title'] and $data['widget_content'] will replace the values in the cached vars using those keys.
I have a function named siblings which fetches all siblings of a user.
select siblings(id) as `siblings` from users where id = 1
I can access the function in Eloquent as
User::where('id', 1)->first([DB::raw(siblings(id) as `siblings`)]->siblings;
I want to make the siblings available via custom attribute.
I added siblings to $appends array
I also created getSiblingsAttribute method in my User model as
public function getSiblingsAttribute()
{
if (!$this->exists()) {
return [];
}
$siblings = User::where('idd', $this->id)
->first([DB::raw('siblings(id) AS `siblings`')])
->siblings;
return explode(',', $siblings);
}
But this is not working as $this->id returns null
My table schema is users(id, username,...), so clearly id is present.
Is there a way by which I can bind the siblings function while querying db and then returning something like $this->siblings from getSiblingsAttribute. If I can bind siblings(id) as siblings with query select globally as we do for scopes using global scope.
That way my code can be simply
public function getSiblingsAttribute()
{
return $this->siblings;
}
The simplest way is to create a view in your database and use that as a table:
protected $table = 'user_view';
Otherwise I need more information about your id == null problem.
If you can fix this by your own in the next step it is important that you use an other column name by selecting as in your accessor otherwise you run in an infinite loop.
public function getSiblingsAttribute()
{
if (!$this->exists()) {
return [];
}
$siblings = User::where('id', $this->id)
->first([DB::raw('siblings(id) AS `siblings_value`')])
->siblings_value;
return explode(',', $siblings);
}
EDIT
Sadly there is no simple way to archieve this.
But after a little bit tinkering I have found a (not very nice) solution.
Give it a try.
You have to add the following class and trait to your app.
app/Classes/AdditionalColumnsTrait.php (additional column trait)
namespace App\Classes;
trait AdditionalColumnsTrait {
public function newEloquentBuilder($query) {
$builder = new EloquentBuilder($query);
$builder->additionalColumns = $this->getAdditionalColumns();
return $builder;
}
protected function getAdditionalColumns() {
return [];
}
}
app/Classes/EloquentBuilder.php (extended EloquentBuilder)
namespace App\Classes;
use Illuminate\Database\Eloquent\Builder;
class EloquentBuilder extends Builder {
public $additionalColumns = [];
public function getModels($columns = ['*']) {
$oldColumns = is_null($this->query->columns) ? [] : $this->query->columns;
$withTablePrefix = $this->getModel()->getTable() . '.*';
if (in_array('*', $columns) && !in_array($withTablePrefix, $oldColumns)) {
$this->query->addSelect(array_merge($columns, array_values($this->additionalColumns)));
} elseif (in_array($withTablePrefix, $oldColumns)) {
$this->query->addSelect(array_values($this->additionalColumns));
} else {
foreach ($this->additionalColumns as $name => $additionalColumn) {
if (!is_string($name)) {
$name = $additionalColumn;
}
if (in_array($name, $columns)) {
if (($key = array_search($name, $columns)) !== false) {
unset($columns[$key]);
}
$this->query->addSelect($additionalColumn);
}
}
if (is_null($oldColumns)) {
$this->query->addSelect($columns);
}
}
return parent::getModels($columns);
}
}
after that you can edit your model like this:
class User extends Model {
...
use App\Classes\AdditionalColumnsTrait;
protected function getAdditionalColumns() {
return [
'siblings' => DB::raw(siblings(id) as siblings)),
];
}
...
}
now your siblings column will be selected by default.
Also you have the option to select only specific columns.
If you don't want to select the additional columns you can use: User::find(['users.*']).
Perhaps it is a solution for you.
I have this code
//ImageableTrait
trait ImageableTrait
{
public function images()
{
return $this->morphMany(Image::class, 'imageable')
->orderBy('order', 'ASC');
}
}
//User
class User extend Model
{
use ImageableTrait;
}
//Post
class Post extend Model
{
use ImageableTrait;
}
class ImageCollection extends Collection
{
public function firstOrDefault()
{
if ($this->count() === 0) {
$image = new Image();
$image->id = 'default';
$image->imageable_type = '/* I need the parent className here */';
$image->imageable_id = '.';
}
return $this->first();
}
}
//Image
class Image extend Model
{
public function imageable
{
return $this->morphTo();
}
public function newCollection(array $models = [])
{
return new ImageCollection($models);
}
public function path($size)
{
//Here, there is some logic to build the image path and it needs
//the imageable_type attribute no matter if there is
//an image record in the database or not
return;
}
}
I want to be able to do so
$path = User::find($id)->images->firstOrDefault()->path('large');
But I can't figure out how to get the parent class name to build the path properly...
I tried with $morphClass or getMorphClass() but can't figure out how to use it properly or if it is even the right way to do it.
Any thoughts on that?
I think you can keep it simple and drop the ImageCollection class because there is already a firstOrNew method that seems to be what you're looking for.
The firstOrNew method accepts an array of attributes that you want to match. If you don't care about the attributes, you can pass an empty array. If there are no matches in the database, it'll make a new instance with the proper parent type.
$path = User::find($id)->images()->firstOrNew([])->path('large');
Note: I am calling the images() method to get the MorphMany object so that I can call the firstOrNew method. In other words, you need to add the parentheses. Otherwise, you get a Collection.
Edit: If you want to make things a bit simpler by automatically setting some default attributes, you can add this to your ImageableTrait:
public function imagesOrDefault()
{
$defaultAttributes = ['id' => 'default'];
return $this->images()->firstOrNew($defaultAttributes);
}
Then, you can do something like this: $path = User::find($id)->imagesOrDefault()->path('large');
Note that your default attributes must be fillable for this to work. Also, imageable_id and imageable_type will automatically be set to your parent's id and type.
If you want to set the default value for imageable_id to a period and not the parent's id, then you have to alter it a bit, and it will look a lot like your original code except this will go inside your ImageableTrait.
public function imagesOrDefault()
{
// First only gets one image.
// If you want to get all images, then change it to get.
// But if you do that, change the conditional check to a count.
$image = $this->images()->first();
if (is_null($image))
{
$image = new Image();
$image->id = 'default';
$image->imageable_type = $this->getMorphClass();
$image->imageable_id = '.';
}
return $image;
}
Ok guys I've found something that seems to work pretty good for now so I'll stick with that.
In the Image model, I've added some code when I make the new collection:
public function newCollection(array $models = [])
{
$morphClass = '';
$parent = debug_backtrace(false, 2)[1];
if (isset($parent['function']) AND $parent['function'] === 'initRelation') {
if (isset($parent['args'][0][0])) {
$morphClass = get_class($parent['args'][0][0]);
}
}
return new ImageCollection($models, $morphClass);
}
I then simply retrieve the morphClass through the constructor of the ImageCollection
private $morphClass;
public function __construct($items = [], $morphClass)
{
parent::__construct($items);
$this->morphClass = $morphClass;
}
public function firstOrDefault()
{
if ($this->count() === 0) {
$image = new Image();
$image->id = 'default';
$image->imageable_type = $this->morphClass;
$image->imageable_id = '.';
}
return $this->first();
}
This way, I can simply call the method like that
User::with('images')->get()->images->firstOrDefault()
This seems to work really great in many cases, if I have some issues at some times, I'll update this post.
i may be late for the party, but i kinda did a small trick for morph relationships where i had "media" as morph, i get the parent since "model_type" has the full string parent class string.
$model = new $media->model_type;
$media->model = $model->findOrFail($media->model_id);
Is there any way to dynamically retrieve a list of "legal" validation rules? I'm trying to have my models self-validate their own validation rule string, to make sure it is accurate. i.e. to make sure someone didn't type "requierd".
I see getRules() at http://laravel.com/api/class-Illuminate.Validation.Validator.html, but that only returns ruled used within the validation, not a list of all known rules.
There's no official API to do this, so you'll need to use reflection. If you look at the implementation of the validate method, you'll see that rules are simply methods on the validate object (that's returned from the static call to make)
#File: vendor/laravel/framework/src/Illuminate/Validation/Validator.php
protected function validate($attribute, $rule)
{
//...
$method = "validate{$rule}";
if ($validatable && ! $this->$method($attribute, $value, $parameters, $this))
{
$this->addFailure($attribute, $rule, $parameters);
}
//...
}
This means we can use reflection to grab a list of validate rules. Also, the method names are camel case with a leading capital letter ("studly case" in laravel speak) so we'll need to lower-case/underscore them ("snake case" in laravel speak) to get the actual validation rule name. We'll also identify which rules have : parameters. Unfortunately, there's no way to derive what each rule expects the parameter to be.
$validator = Validator::make(array(), array());
//
$r = new ReflectionClass($validator);
$methods = $r->getMethods();
//filter down to just the rules
$methods = array_filter($methods, function($v){
if($v->name == 'validate') { return false; }
return strpos($v->name, 'validate') === 0;
});
//get the rule name, also if it has parameters
$methods = array_map(function($v){
$value = preg_replace('%^validate%','',$v->name);
$value = Str::snake($value);
$params = $v->getParameters();
$last = array_pop($params);
if($last && $last->name == 'parameters')
{
$value .= ':[params]';
}
return Str::snake($value);
}, $methods);
var_dump($methods);
If a user has added validation rules by extending the validation class, this technique will pickup any custom methods. However, if a user has extended the validation class with the Validation::extend syntax, the technique above will not find those rule. To get those rules, you'll need to do something like this.
Validator::extend('customrule',function($attribute, $value, $parameters){
});
Validator::extend('anothercustom', 'FooValidator#validate');
$validator = Validator::make(array(), array());
$extension_methods = array();
foreach($validator->getExtensions() as $value=>$callback)
{
if(is_string($callback))
{
list($class, $method) = explode('#', $callback);
$r = new ReflectionClass($class);
$method = $r->getMethod($method);
}
else if(is_object($callback) && get_class($callback) == 'Closure')
{
$method = new ReflectionFunction($callback);
}
$params = $method->getParameters();
$last = array_pop($params);
if($last && $last->name == 'parameters')
{
$value .= ':[params]';
}
$extension_methods[] = $value;
}
I set the name of my table in my model in the constructor:
public function __construct($section, $attributes = array(), $exists = false){
parent::__construct($attributes, $exists);
$this->table = $section;
}
Later on in the class I use this method:
public function getEdit($id){
return $this->find($id);
}
But it fails:
Missing argument 1 for MyModel::__construct()
Any ideas where I'm going wrong?
If you ever override a method that is provided by another class, you should add your own parameters to the end of the parameter list as optional parameters. The reason being that there will likely be code that relies on the old implementation. You can't expect it to know your new implementation, so you must add your own stuff to the end (and optional because the existing implementation won't be supplying it).
As such, change your constructor to:
public function __construct($attributes = array(), $exists = false, $section = null){
parent::__construct($attributes, $exists);
$this->table = $section;
}
And you should find it works. Obviously you'll have to update your code to pass $seciton as the last parameter now, too.