Sonata admin - Sorting by translated property - sonata-admin

I have a code:
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('name')
[..]
This is a property from translation (KNP translatable). I tried use:
translations.name - label is sortable, but values are missing
name or translate.name - label is not sortable, but values are ok
I don't have any idea how I should to do this. Maybe someone here can help me?

Did you try $listMapper->add('name',null, array('sortable'=>true)) ?

Ok, I made it.
1) Create abstract admin class:
use Sonata\AdminBundle\Admin\AbstractAdmin as BaseAbstractAdmin;
abstract class AbstractAdmin extends BaseAbstractAdmin { .. }
2) Use this class in your admin classes:
class UserAdmin extends AbstractAdmin { .. }
3) Add this to your column definition:
->add(
'fieldName',
null,
[
'sortable' => true,
'sort_field_mapping' => ['fieldName' => 'id'],
'sort_parent_association_mappings' => [],
]
)
4) Add this method to your abstract admin class:
protected function prepareQueryForTranslatableColumns($query)
{
$currentAlias = $query->getRootAliases()[0];
$locale = $this->request->getLocale();
$parameters = $this->getFilterParameters();
$sortBy = $parameters['_sort_by'];
$fieldDescription = $this->getListFieldDescription($sortBy);
$mapping = $fieldDescription->getAssociationMapping();
$entityClass = $mapping['targetEntity'] ?: $this->getClass();
if ($mapping) {
$mappings = $fieldDescription->getParentAssociationMappings();
$mappings[] = $mapping;
foreach ($mappings as $parentMapping) {
$fieldName = $parentMapping['fieldName'];
$query->leftJoin($currentAlias . '.' . $fieldName, $fieldName);
$currentAlias = $fieldName;
}
}
$query
->leftJoin(
$currentAlias . '.translations',
'tr',
'with',
'tr.locale = :lang OR
(NOT EXISTS(SELECT t.id FROM ' . $entityClass . 'Translation t WHERE t.translatable = tr.translatable AND t.locale = :lang)
AND tr.locale = :lang_default)'
)
->addOrderBy('tr.name', $parameters['_sort_order'])
->setParameter(':lang', $locale)
->setParameter(':lang_default', 'en');
return $query;
}
I use JOIN to get translations for currently selected locale and, if translation doesn't exist yet for current locale, I add translation for default locale (it is a reason for use NOT EXIST).
5) Add this method to your admin class:
public function createQuery($context = 'list')
{
$query = parent::createQuery($context);
if ('list' === $context) {
$parameters = $this->getFilterParameters();
$sortBy = $parameters['_sort_by'];
if (in_array($sortBy, ['fieldName', 'fieldName.fieldName2', 'fieldName3', ..])) {
$query = parent::prepareQueryForTranslatableColumns($query);
}
}
return $query;
}

Late answer but I was having the same problem.
The easiest solution for me was to set the right property mapping like this:
$listMapper->add(
'translations',
null,
[
'sortable' => true,
'associated_property' => 'name',
'sort_field_mapping' => [
'fieldName' => 'name',
],
'sort_parent_association_mappings' => [
['fieldName' => 'translations'],
],
]
);

Related

CakePHP Query Builder 4.x for SQL INSERT INTO IF NOT EXISTS

This CakePHP Query isn't using the conditional, $subQuery for some reason:
$subQuery = $this->queryFactory->newSelect('table_name')
->select(['id'])
->where(['id' => $id]);
$query = $this->queryFactory->newQuery()
->insert(
['id', 'machine', 'logfile', 'updated', 'time']
)
->into('table_name')
->values([
'id' => $id,
'machine' => $machine['id'],
'logfile' => $logFile,
'updated' => $updateDate,
'time' => $updateTime
])
->where(function (QueryExpression $exp) use ($subQuery) {
return $exp->notExists($subQuery);
});
$query->execute();
...it just inserts record even when it exists, but why?
The above code is only part of the required SQL that looks like this:
IF NOT EXISTS(
SELECT 1
FROM table_name
WHERE id = '$id'
)
INSERT INTO table_name (id, machine, logfile, updated, time)
VALUES (?,?,?,?,?)
ELSE
UPDATE table_name
SET updated = '$var1', time = ' $var2'
WHERE id = '$id';
There is no API that would allow to generate such a statement directly, the query builder isn't ment to generate (and execute) such SQL constructs, it can only compile SELECT, INSERT, UPDATE, and DELETE queries, and while the query expression builder can be used to stitch together arbitrary expressions, it will wrap itself and query objects into parentheses (as it is meant for use in query objects), which would be incompatible with what you're trying to build.
So if you want to run such constructs on SQL level, then you either have to write the SQL manually, or create custom expression classes that can build such constructs. In any case you would have to run the SQL manually then.
Here's a very basic quick & dirty example of such a custom expression class:
namespace App\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
use Closure;
class IfElseExpression implements ExpressionInterface
{
protected $_if;
protected $_then;
protected $_else;
public function if(ExpressionInterface $expression)
{
$this->_if = $expression;
return $this;
}
public function then(ExpressionInterface $expression)
{
$this->_then = $expression;
return $this;
}
public function else(ExpressionInterface $expression)
{
$this->_else = $expression;
return $this;
}
public function sql(ValueBinder $binder): string
{
$if = $this->_if->sql($binder);
$then = $this->_then->sql($binder);
$else = $this->_else->sql($binder);
return "IF $if $then ELSE $else";
}
public function traverse(Closure $callback)
{
$callback($this->_if);
$this->_if->traverse($callback);
$callback($this->_then);
$this->_then->traverse($callback);
$callback($this->_else);
$this->_else->traverse($callback);
return $this;
}
public function __clone()
{
$this->_if = clone $this->_if;
$this->_then = clone $this->_then;
$this->_else = clone $this->_else;
}
}
It could then be used something like this:
$notExists = (new \Cake\Database\Expression\QueryExpression())
->notExists($subQuery);
$insertQuery = $this->queryFactory->newQuery()
->insert(/* ... */)
//...
;
$updateQuery = $this->queryFactory->newQuery()
->update(/* ... */)
//...
;
$ifElse = (new \App\Database\Expression\IfElseExpression())
->if($notExists)
->then($insertQuery)
->else($updateQuery);
$binder = new \Cake\Database\ValueBinder();
$sql = $ifElse->sql($binder);
$statement = $connection->prepare($sql);
$binder->attachTo($statement);
$statement->execute();
See also
Cookbook > Database Access & ORM > Database Basics > Interacting with Statements
Yes, thanks. My own preference is to avoid the requirement to code the value binding explicitly. Using where(), I can do something like this:
$subQuery = $this->queryFactory->newSelect('table_name')
->select(['id'])
->where(['id' => $id])
->limit(1);
$find = $subQuery->execute()->fetchAll('assoc');
if (!empty($find)) {
$values = [
'id' => $id,
'machine' => $machine,
'logfile' => $logFile,
'updated' => $var1,
'time' => $var2
];
$query = $this->queryFactory->newInsert('table_name', $values);
} else {
$query = $this->queryFactory->newUpdate('table_name')
->set([
'updated' => $someVar,
'time' => $someVar2
])
->where(['id' => $id]);
}
$query->execute();

Object of class could not be converted to string of class CI_DB_mysqli_result

Controller :
function index()
{
$this->load->helper(array('form', 'url','common'));
$this->load->library(array('session','pagination'));
$this->load->model('productsdisplay','',TRUE);
$data=array(
'header' => $this->load->view('frontend/assets/header', '', TRUE),
'footer' => $this->load->view('frontend/assets/footer', '', TRUE),
);
$item= $this->input->get('products', TRUE);
$newarray[]=array();
$item_id =$this->db->query('select id from item where slug=(".$item.")');
$result=$item_id->result();
var_dump($result);
$data['category']=$this->productsdisplay->getAllCategories($item_id);
$data['kvas']=$this->productsdisplay->getAllKva();
$data['items']=$this->productsdisplay->getAllItems();
$data['applications']=$this->productsdisplay->getAllApplication();
$data['products'] = $this->productsdisplay->getAllProduct();
$data['ads'] = $this->productsdisplay->getAllAds();
$insert_id=$this->productsdisplay->addEnquiry($this->input->post());
$this->load->view('frontend/product/index',$data);
}
Model :
function getAllCategories($item_id)
{
if($item_id!=''){
$this->db->select('category_name');
$this->db->order_by("category_name", "ASC");
$query = $this->db->where('product_type',$item_id);
$query = $this->db->get('category');
return $query->result();
}
}
I am not able to understand how to solve this error. Please help me regarding the issue.
I am a beginner in CI.
The query which gets $item_id is wrong. It should be changed as follows
$item_id =$this->db->query('select id from item where slug='.$item);

How to insert into a table based on an Eloquent relationship an array of foreign keys

I have two models TeamMember and ProjectRequest.
A TeamMember can have one ProjectRequest, that is why I created the following Eloquent relationship on TeamMember:
class TeamMember extends Model {
//
protected $table = 'team_members';
protected $fillable = ['project_request_id'];
// Relations
public function projectTeam() {
return $this->hasOne('\App\Models\ProjectRequest', 'project_request_id');
}
}
In my Controller I want to query both tables, however it returns the failure message.
What is important to know is that $request->projectTeam is an array of emails, looking like this:
array:2 [
0 => "mv#something.com"
1 => "as#something.com"
]
Meaning that I need to bulk insert into team_members table the project_request_ id for each team member where the emails are in the array.
How can I do that in the right way? The following is my attempt:
public function createProjectTeam(Request $request){
try {
$title = $request->projectTitle;
$TeamMember = $request->projectTeam;
$projectRequest = ProjectRequest::create(['project_title' => $title]);
$projectRequestId = $projectRequest->id;
$projectTeam = $this->teamMembers->projectTeam()->create(['project_request_id'=> $projectRequestId])->where('email', $TeamMember);
//$projectTeam = TeamMember::createMany(['project_request_id' => $projectRequestId])->where($TeamMember);
//dd($projectTeam);
return $projectRequest.$projectTeam;
} catch(\Exception $e){
return ['success' => false, 'message' => 'project team creation failed'];
}
}
There are a few things you can do.
Eloquent offers a whereIn() method which allows you to query where a field equals one or more in a specified array.
Secondly, you can use the update() method to update all qualifying team members with the project_request_id:
public function createProjectTeam(Request $request)
{
try {
$projectRequest = ProjectRequest::create(['project_title' => $request->projectTitle]);
TeamMember::whereIn('email', $request->projectTeam)
->update([
'project_request_id' => $projectRequest->id
]);
return [
'success' => true,
'team_members' => $request->projectTeam
];
} catch(\Exception $e) {
return [
'success' => false,
'message' => 'project team creation failed'
];
}
}
I hope this helps.

Cakephp 3 PHPUnit ingration test fails after the test before

I have the UsersFixture with three records.
The test methods first() and second(), which both are before guest_can_login(), pr's show "Joe", "Joe", as expected. But with test method third(), which comes after guest_can_login(), I get notice error: trying to get property of non-object.
So, for a reason, something in the guest_can_login() breaks the rest of the test methods. I have tried by making a duplicate of guest_can_login() as well.
I think it is strange, as tearDown should "reset" everything after each test. I'm out of ideas. And after reading the Cakephp Testing docs, I haven't been able to solve it.
Any suggestions to help me solve this is much appreciated.
Code below (gist if you prefer: https://gist.github.com/chris-andre/2eb3ad053073caf4f1c81722428a900b):
public $fixtures = [
'app.users',
'app.tenants',
'app.roles',
'app.roles_users',
];
public $Users;
public function setUp()
{
parent::setUp();
$config = TableRegistry::getTableLocator()->exists('Users') ? [] : ['className' => UsersTable::class];
$this->Users = TableRegistry::getTableLocator()->get('Users', $config);
}
/**
* tearDown method
*
* #return void
*/
public function tearDown()
{
unset($this->Users);
TableRegistry::clear();
parent::tearDown();
}
/** #test */
public function guest_can_register()
{
$this->enableCsrfToken();
$this->enableSecurityToken();
$this->configRequest([
'headers' => [
'host' => 'timbas.test'
]
]);
$data = [
'email' => 'chris#andre.com',
'first_name' => 'Christian',
'last_name' => 'Andreassen',
'password' => '123456',
'tenant' => ['name' => 'Test Company AS', 'domain' => 'testcomp', 'active' => true],
'active' => true
];
$this->post('/register', $data);
$this->assertResponseSuccess();
$this->assertRedirect(['controller' => 'Users', 'action' => 'login', '_host' => 'testcomp.timbas.test']);
$user = $this->Users->find()
->contain(['Tenants', 'Roles'])
->where(['Users.email' => 'chris#andre.com'])
->first();
}
/** #test */
public function first()
{
$users = $this->Users->find()->first();
pr($users->first_name);
}
/** #test */
public function second()
{
$users = $this->Users->find()->first();
pr($users->first_name);
}
/** #test */
public function guest_can_login()
{
$this->enableCsrfToken();
$this->enableSecurityToken();
$this->configRequest([
'headers' => [
'host' => 'testcomp.timbas.test'
]
]);
$data = [
'email' => 'chris#andre.com',
'first_name' => 'Christian',
'last_name' => 'Andreassen',
'password' => '123456',
'tenant' => ['name' => 'Test Company AS', 'domain' => 'testcomp', 'active' => true],
'active' => true,
'roles' => ['_ids' => [ADMINISTRATOR_ROLE_ID]]
];
$user = $this->Users->newEntity($data, [
'associated' => ['Tenants', 'Roles']
]);
$this->Users->save($user);
$getNewUser = $this->Users->find()
->contain(['Roles'])
->where(['Users.email' => 'chris#andre.com'])
->first()
->toArray();
// pr($getNewUser->id);
$this->post('/users/login', [
'email' => 'chris#andre.com',
'password' => '123456'
]);
$this->assertSession($getNewUser, 'Auth.User');
}
/** #test */
public function third()
{
$users = $this->Users->find()->first();
pr($users->first_name);
}
EDIT 2018-08-06:
Users::register() is a global context, I cannot be accessed from url with subdomain. E.g. tenant1.domain.com/register will throw a badRequest, while domain.com/register is a valid url. On registration success, user is forwarded to login from right url. Login-url = Tenants.domain + domain + suffix, e.g. tenant1.domain.com. When user is on the tenant scope (url with subdomain), the Tenants.id where Tenants.domain = tenant1, will be added to the where clause in all queries for the models having the behavior attached.
Now, what happens in third() is that the tenant_id from the newly created Tenant in guest_can_login() is added to the query, which means the test "is still on the tenant scope" when third() is run. That is the problem.
The other problem is that setUp() is called on all test methods but third(). testDown() is called on every test methods.
App\Middleware\TenantMiddleware.php:
use InstanceConfigTrait;
/**
* Default config.
* Options:
* - globalScope: tells the middleware what controller and action tenant scope is not being used
* Example
* 'globalScope' => [
* 'Pages' => ['*'], // All actions in PagesController is global
* 'Users' => ['register'] // Register action in UsersController is global
* ]
* #var array
*/
protected $_defaultConfig = [
'globalScope' => [
'Users' => ['register'],
'Landing' => ['*'],
'Pages' => ['*']
],
];
public function __construct($config = [])
{
if (!isset($config['primaryDomain'])) {
$config['primaryDomain'] = Configure::read('Site.domain');
}
$this->setConfig($config);
}
/**
* Invoke method.
*
* #param \Cake\Http\ServerRequest $request The request.
* #param \Psr\Http\Message\ResponseInterface $response The response.
* #param callable $next Callback to invoke the next middleware.
* #return \Psr\Http\Message\ResponseInterface A response
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
{
// Get subdomains
$subdomains = $request->subdomains();
// If subdomains not empty, the first is always the tenants domain
$subdomain = !empty($subdomains) ? $subdomains[0] : '';
Tenant::setDomain($subdomain);
// Get params of current request
$params = $request->getAttribute('params');
$controller = $params['controller'];
$action = $params['action'];
// Set tenantScope as default
Tenant::setScope('tenant');
$globalScope = $this->getConfig('globalScope');
// If Controller and action is a global scope
if (array_key_exists($controller, $globalScope)) {
if (in_array($action, $globalScope[$controller]) || in_array('*', $globalScope[$controller])) {
Tenant::setScope('global');
}
}
if (
(Tenant::getScope() === 'tenant' && Tenant::tenant() === null)
|| (Tenant::getDomain() === '' && Tenant::getScope() === 'tenant')
|| (Tenant::getDomain() !== '' && Tenant::getScope() === 'global')
) {
throw new NotFoundException('The page you are looking for does not exists.');
}
$primaryDomain = $this->getConfig('primaryDomain');
if (array_key_exists($controller, $globalScope)) {
if (in_array($action, $globalScope[$controller]) && Tenant::getScope() === 'global') {
}
}
return $next($request, $response);
}
App\Model\Behavior\TenantScopeBehavior.php:
protected $_table;
/**
* Default configuration.
*
* #var array
*/
protected $_defaultConfig = [];
public function __construct(Table $table, array $config = [])
{
parent::__construct($table, $config);
}
public function beforeFind(Event $event, Query $query, ArrayObject $options)
{
$model = $this->_table->getAlias();
$foreig_key = 'tenant_id';
if (!isset($options['skipTenantCheck']) || $options['skipTenantCheck'] !== true) {
if (Tenant::getScope() === 'tenant') {
if ($model === 'Tenants') {
$query->where(['Tenants.id' => Tenant::tenant()->id]);
} else {
$query->where([$model . '.' . $foreig_key => Tenant::tenant()->id]);
}
}
}
return $query;
}
public function beforeSave(Event $event, Entity $entity, $options)
{
if (Tenant::getScope() === 'tenant') {
if ($entity->isNew()) {
$entity->tenant_id = Tenant::tenant()->id;
} else {
// Check if current tenant is owner
if ($this->_table->getAlias() === 'Tenants') {
if ($entity->id != Tenant::tenant()->id) {
throw new BadRequestException();
}
} else {
if ($entity->tenant_id != Tenant::tenant()->id) {
throw new BadRequestException();
}
}
}
}
return true;
}
public function beforeDelete(Event $event, Entity $entity, $options)
{
if (Tenant::getScope() === 'tenant') {
if ($entity->tenant_id != Tenant::tenant()->id) { //current tenant is NOT owner
throw new BadRequestException();
}
}
return true;
}
App\Tenant\Tenant.php:
/**
* $_domain will be empty or comtain the domain (subdomain from url)
* #var string can be empty
*/
protected static $_domain;
/**
* $_scope shall be 'global' or 'tenant'.
* #var string
*/
protected static $_scope;
/**
* #var null|object \App\Model\Entity\Tenant
*/
protected static $_tenant;
/**
* Gets domain from $_domain and returns the string
* #return string
*/
public static function getDomain()
{
return self::$_domain;
}
/**
* Set the tenant scope domain. Will be set in the TenantMiddleware, and shall not be set anywhere else
* #param string $domain
* #return string empty or with domain
*/
public static function setDomain($domain)
{
self::$_domain = $domain;
}
/**
* Tenant method
* Return the object \App\Model\Table\Tenants ro null
* #return type
*/
public static function tenant()
{
$tenant = static::_getTenant();
return $tenant;
}
protected static function _getTenant()
{
if (self::$_tenant === null) {
$cachedTenants = Cache::read('tenants');
if($cachedTenants !== false) {
// do something
}
$tenantsTable = TableRegistry::get('Tenants');
$tenant = $tenantsTable->find('all', ['skipTenantCheck' => true])
->where(['Tenants.domain' => self::getDomain()])
->where(['Tenants.active' => true])
->first();
self::$_tenant = $tenant;
}
return self::$_tenant;
}
public static function getScope()
{
return self::$_scope;
}
/**
* Description
* #param type $scope
* #return type
*/
public static function setScope($scope)
{
self::$_scope = $scope;
}

Decoding JSON array laravel

Hello i am having slight trouble with arrays in my application. I have a table in my database called "contacts" inside that table i have a column "info" of datatype 'TEXT' where i store a json encoded array. This is my function:
/**
* Store a newly created resource in storage.
*
* #return Response
*/
public function store(Request $request)
{
$contact = new Contacts;
$contact->type = $request->type;
$contact->name = str_replace(' ', ' ', $request->type == 'person' ? $request->first_name . ' ' . $request->other_name . ' ' . $request->last_name : $request->description);
$contact->email = $request->email;
$contact->info = json_encode($request->info);
if ($contact->save())
{
return [
'success' => 'Data was saved successfully!'
];
}
}
Now this saves perfectly however the issues is when i retrieving the data from the database it returns that column as a "string" and as such i am unable to access that array from my front-end javascript (i use angular).
I have tried decoding the entire returned result but that did not work, still had the same problem:
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index()
{
$type = Input::get('type');
$contacts = "";
if (empty($type))
{
$contacts = Contacts::get();
}
elseif ($type == 'persons')
{
$contacts = Contacts::where('type', 'person')->get();
}
elseif ($type == 'companies')
{
$contacts = Contacts::where('type', 'company')->get();
}
return [
'contacts' => json_decode($contacts)
];
}
Use the $casts property on your model:
class Contacts extends Eloquent
{
protected $casts = ['inof' => 'json'];
}
It'll handle all encoding/decoding for you.
$contact->info = $request->info;

Resources