Query an entity in a different Language - typo3

I'm trying to query entietes in a language, other than the system default one, my repository method looks like this,
public function findOneByMaterialnumber( $materialnumber, $sysLanguageUid ){
$query = $this->createQuery();
$query->matching($query->like('materialnumber',$materialnumber));
$query->getQuerySettings()->setIgnoreEnableFields(true);
$query->getQuerySettings()->setRespectStoragePage(false);
$query->getQuerySettings()->setLanguageUid($sysLanguageUid);
//$query->getQuerySettings()->setRespectSysLanguage(false);
//$query->getQuerySettings()->setLanguageMode('strict');
//$query->getQuerySettings()->setLanguageOverlayMode(false);
$parser = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Storage\\Typo3DbQueryParser');
$queryParts = $parser->parseQuery($query);
//\TYPO3\CMS\Core\Utility\DebugUtility::debug($queryParts, 'Query');
print_r($queryParts);
exit;
return $query->execute()->getFirst();
}
but the query this results in still incorporates the sys_language_uid in the non-strict way. The debugged query object looks like this.
[keywords] => Array
(
)
[tables] => Array
(
[tx_productfinder_domain_model_product] => tx_productfinder_domain_model_product
)
[unions] => Array
(
)
[fields] => Array
(
[tx_productfinder_domain_model_product] => tx_productfinder_domain_model_product.*
)
[where] => Array
(
[0] => tx_productfinder_domain_model_product.materialnumber LIKE :
)
[additionalWhereClause] => Array
(
[0] => (tx_productfinder_domain_model_product.sys_language_uid IN (0,-1))
)
[orderings] => Array
(
[0] => tx_productfinder_domain_model_product.ordercode ASC
[1] => tx_productfinder_domain_model_product.title ASC
[2] => tx_productfinder_domain_model_product.materialnumber ASC
)
[limit] =>
[offset] =>
[tableAliasMap] => Array
(
[tx_productfinder_domain_model_product] => tx_productfinder_domain_model_product
)
This happens regardless of the sys_language_uid i query for. What am I doing wrong?
As you can see, I've tried combinations of all sorts of QuerySettings, setRespectSysLanguage, setLanguageMode and setLanguageOverlayMode. As I understand it, I have to query strictly and without language overlay. But none of those, nor their combinations, worked as intended.

This works with TYPO3 8.7.xx
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
/**
* The repository for Products
*/
class ProductRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
/*
* #param int $uid id of record
* #param int $langUid sys_language_uid of the record
* #return \ITSHofmann\ItsProductfile\Domain\Mpdell\Product
*/
public function findByUidAndLanguageUid($uid,$langUid)
{
$query = $this->createQuery();
$object = $query->matching(
$query->equals('uid', $uid)
)->execute()->getFirst();
if ($object) {
$className = get_class ($object);
$dataMapper = $this->objectManager->get(DataMapper::class);
$tableName = $dataMapper->getDataMap($className)->getTableName();
$transOrigPointerField = $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'];
$languageField = $GLOBALS['TCA'][$tableName]['ctrl']['languageField'];
$connection = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connection->getQueryBuilderForTable($tableName );
$statement = $queryBuilder->select('*')
->from($tableName )
->where(
$queryBuilder->expr()->eq(
$transOrigPointerField,$queryBuilder->createNamedParameter($object->getUid(), \PDO::PARAM_INT)),
$queryBuilder->expr()->eq(
$languageField,$queryBuilder->createNamedParameter($langUid, \PDO::PARAM_INT))
)->execute();
$objectRow = $statement ->fetch();
if ($objectRow) {
$langObject = $dataMapper->map($className,[$objectRow]);
if ($langObject ) {
return reset ($langObject );
}
}
}
return $object;
}
}

edit. The answer I posted here originally turned out not to work either. What I ended up doing instead, was to write a custom statement, execute it to return the raw data and map it, via DataMapper->map(); Refer to Christop Hofmanns answer for details.

Related

TYPO3/Extbase How to create unique slug within create action?

I have slug field in my TCA and in general it works, when adding via Backend > List module, even if I won't input any value the unique eval ensures that slug will be unique, so when I'll create many rows with the same name Foo TYPO3 backend will enshure that it will resolve to unique slugs like foo, foo-1, foo-2, etc. Kudos!:
'slug' => [
'exclude' => true,
'label' => 'Slug',
'displayCond' => 'VERSION:IS:false',
'config' => [
'type' => 'slug',
'generatorOptions' => [
'fields' => ['name'],
'fieldSeparator' => '/',
'replacements' => [
'/' => '',
],
],
'fallbackCharacter' => '-',
'eval' => 'unique',
'default' => '',
'appearance' => [
'prefix' => \BIESIOR\Garage\UserFunctions\SlugPrefix::class . '->getPrefix'
],
],
],
However when creating a new object from my form within new/create actions (typical Extbase CRUD from extension_builder as you can see) like:
public function createAction(Car $newCar)
{
$this->addFlashMessage(
'The object was created. Please be aware that this action is publicly accessible unless you implement an access check. See https://docs.typo3.org/typo3cms/extensions/extension_builder/User/Index.html',
'',
\TYPO3\CMS\Core\Messaging\AbstractMessage::WARNING);
$this->carRepository->add($newCar);
$this->redirect('list');
}
of course slug is note set.
My first idea is to duplicate the logic of TCA type='slug' and just add this functionality with some own JS, AJAX and PHP, however that sounds as overload and time consumption. Especially that I don't want the user to care about slug part at all. Is there any simple API for lookup for a unique slug of the given table that can be used in custom action instead?
Note this question is not about how to handle it with JS, that's just concept. I would like to skip this part for FE user at all, he doesn't need to know what the slug is. Just during creating a new object, I want to get unique value like foo-123 instead.
In addition to Jonas Eberles answer here's another example which also respects the eval configuration of the slug field (can be uniqueInSite, uniqueInPid or simply unique).
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
...
public function createAction(Car $newCar)
{
$this->carRepository->add($newCar);
GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager::class)->persistAll();
$record = $this->carRepository->findByUidAssoc($newCar->getUid())[0];
$tableName = 'tx_garage_domain_model_car';
$slugFieldName = 'slug';
// Get field configuration
$fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$slugFieldName]['config'];
$evalInfo = GeneralUtility::trimExplode(',', $fieldConfig['eval'], true);
// Initialize Slug helper
/** #var SlugHelper $slugHelper */
$slugHelper = GeneralUtility::makeInstance(
SlugHelper::class,
$tableName,
$slugFieldName,
$fieldConfig
);
// Generate slug
$slug = $slugHelper->generate($record, $record['pid']);
$state = RecordStateFactory::forName($tableName)
->fromArray($record, $record['pid'], $record['uid']);
// Build slug depending on eval configuration
if (in_array('uniqueInSite', $evalInfo)) {
$slug = $slugHelper->buildSlugForUniqueInSite($slug, $state);
} else if (in_array('uniqueInPid', $evalInfo)) {
$slug = $slugHelper->buildSlugForUniqueInPid($slug, $state);
} else if (in_array('unique', $evalInfo)) {
$slug = $slugHelper->buildSlugForUniqueInTable($slug, $state);
}
$newCar->setSlug($slug);
$this->carRepository->update($newCar);
}
with custom finder in the repository to fetch assoc array instead of the mapped object for $racord argument
public function findByUidAssoc($uid)
{
$query = $this->createQuery();
$query->matching(
$query->equals('uid', $uid)
);
return $query->execute(true)[0];
}
Note that the record needs to be persisted before executing above code.
References:
SlugHelper::generate
SlugHelper::buildSlugForUniqueInSite
SlugHelper::buildSlugForUniqueInPid
SlugHelper::buildSlugForUniqueInTable
According to answers from Elias and Jonas, I created a class which simplifies things especially when you have more models to handle
typo3conf/ext/sitepackage/Classes/Utility/SlugUtility.php
<?php
namespace VENDOR\Sitepackage\Utility; // <- to be replaced with your namespace
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/***
*
* This file is part of the "Sitepackage" Extension for TYPO3 CMS.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* (c) 2020 Marcus Biesioroff <biesior#gmail.com>
* Concept by: Elias Häußler
* Jonas Eberle
*
***/
class SlugUtility
{
/**
* #param int $uid UID of record saved in DB
* #param string $tableName Name of the table to lookup for uniques
* #param string $slugFieldName Name of the slug field
*
* #return string Resolved unique slug
* #throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
*/
public static function generateUniqueSlug(int $uid, string $tableName, string $slugFieldName): string
{
/** #var Connection $connection */
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName);
$queryBuilder = $connection->createQueryBuilder();
$record = $queryBuilder
->select('*')
->from($tableName)
->where('uid=:uid')
->setParameter(':uid', $uid)
->execute()
->fetch();
if (!$record) return false;
// Get field configuration
$fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$slugFieldName]['config'];
$evalInfo = GeneralUtility::trimExplode(',', $fieldConfig['eval'], true);
// Initialize Slug helper
/** #var SlugHelper $slugHelper */
$slugHelper = GeneralUtility::makeInstance(
SlugHelper::class,
$tableName,
$slugFieldName,
$fieldConfig
);
// Generate slug
$slug = $slugHelper->generate($record, $record['pid']);
$state = RecordStateFactory::forName($tableName)
->fromArray($record, $record['pid'], $record['uid']);
// Build slug depending on eval configuration
if (in_array('uniqueInSite', $evalInfo)) {
$slug = $slugHelper->buildSlugForUniqueInSite($slug, $state);
} else if (in_array('uniqueInPid', $evalInfo)) {
$slug = $slugHelper->buildSlugForUniqueInPid($slug, $state);
} else if (in_array('unique', $evalInfo)) {
$slug = $slugHelper->buildSlugForUniqueInTable($slug, $state);
}
return $slug;
}
}
Usage in any place, like controller. Scheduler task, repository, etc. Keep in mind that record should be saved before (it may be created by Extbase, or just with plain SQL), just need to have created uid and be valid TYPO3 record.
use VENDOR\Sitepackage\Utility\SlugUtility;
use \TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager;
...
$pageSlug = SlugUtility::generateUniqueSlug(
5, // int $uid UID of record saved in DB
'pages', // string $tableName Name of the table to lookup for uniques
'slug' // string $slugFieldName Name of the slug field
)
// or
$uniqueSlug = SlugUtility::generateUniqueSlug(
123,
'tx_garage_domain_model_car',
'slug'
);
// or according to the original question,
// if you created new model object with Extbase,
// persist it, create unique slug with SlugUtility
// set the slug property to the created model object and finally update
public function createAction(Car $newCar)
{
$this->carRepository->add($newCar);
GeneralUtility::makeInstance(PersistenceManager::class)->persistAll();
$uniqueSlug = SlugUtility::generateUniqueSlug(
$newCar->getUid(),
'tx_garage_domain_model_car',
'slug'
);
if($uniqueSlug) {
$newCar->setSlug($uniqueSlug);
$this->carRepository->update($newCar);
}
$this->redirect('list');
}
// no need for second call to persistAll()
// as Extbase will call it at action's finalizing.
// etc.
You can use the SlugHelper directly. The API was obviously not made very fluent for that use case but it works...
$this->carRepository->add($newCar);
// probably you need to persist first - I am not sure if this is really necessary
$this->objectManager()->get(
\TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager::class
)->persistAll();
$table = 'tx_garage_domain_model_car';
$field = 'slug';
// a stripped down record with just the necessary fields is enough
$record = ['name' => $newCar->getName()];
$pid = $this->settings->persistence->...
$slugHelper = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
\TYPO3\CMS\Core\DataHandling\SlugHelper::class,
$table,
$field,
$GLOBALS['TCA'][$table]['columns'][$field]['config']
);
$newCar->slug = $slugHelper->generate($record, $pid);

How to return results from a mysqli query within a class and format them outside the class

I'm gettings started using mysqli prepared statements and am trying to write a class method that retrieves records matching certain date criteria, but does not display them -- i want to be able to format the result outside of the class.
i have the class method working when it displays the results:
public function periodReceipts(){
global $db;
if($query = $db->prepare("SELECT * FROM receipts WHERE date BETWEEN ? AND ? ORDER BY date ASC")){
$query->bind_param("ss", $this->date1, $this->date2);
$query->execute();
$query->bind_result($id, $user_id, $vendor, $amount, $cat, $date);
$query->fetch();
while($query->fetch()){
echo "$id, $user_id, $vendor, $amount, $cat, $date <br/>";
}
$query->close();
}
}
and i have a similar method working that performs a mysqli query, retrieves records, calculates a total, and returns that result, unformatted, to display later:
public function runningTotal(){
global $db;
if($query = $db->prepare("SELECT amount FROM receipts WHERE user_id = ?")){
$query->bind_param("i", $this->uid);
$query->execute();
$query->bind_result($amount);
$running_total = 0;
while($query->fetch()){
$running_total += $amount;
}
$query->close();
}
return $running_total;
$db->close();
}
but i can't figure out how to get the method periodReceipts() to behave similarly. I assume i need to get the data into an array, but how do i do that, and how do i access it later?
thanks!
Change your method like following
...
$result = array();
while($query->fetch()){
$result[] = array(
'id' => $id,
'user_id' => $user_id,
'vendor' => $vendor,
'amount' => $amount,
'cat' => $cat,
'date' => $date
);
}
$query->close();
return $result;
...
Then you'll get all your records in array:
$result = $your_class_instance->periodReceipts();
var_dump($result);

Identity and Credential in different tables. How to login user?

I'm using Zend Framework.
I save users information in two tables.
I have one table for his basic information and password, and in the other table I save his e-mails.
He can login with any e-mail.
My question is how should I extend Zend_Auth_Adapter_DbTable so that I can allow this?
I prefer not to use table views.
[edit]
I found a solution. What worked for me:
class My_Auth_Adapter_DbTable extends Zend_Auth_Adapter_DbTable
{
protected function _authenticateCreateSelect()
{
// build credential expression
if (empty($this->_credentialTreatment) || (strpos($this->_credentialTreatment, '?') === false)) {
$this->_credentialTreatment = '?';
}
$credentialExpression = new Zend_Db_Expr(
'(CASE WHEN ' .
$this->_zendDb->quoteInto(
$this->_zendDb->quoteIdentifier($this->_credentialColumn, true)
. ' = ' . $this->_credentialTreatment, $this->_credential
)
. ' THEN 1 ELSE 0 END) AS '
. $this->_zendDb->quoteIdentifier(
$this->_zendDb->foldCase('zend_auth_credential_match')
)
);
// get select
//$dbSelect = clone $this->getDbSelect();
$mdl = new My_Model_Db_Table_Users();
$dbSelect = $mdl->select();
$dbSelect = $dbSelect->setIntegrityCheck(false);
$dbSelect = $dbSelect->from(array('u' => $this->_tableName), array('*', $credentialExpression));
$dbSelect = $dbSelect->joinInner(array('ue' => 'users_emails'), 'ue.id_user = u.user_id', array('user_email'));
$dbSelect = $dbSelect->where('ue.' . $this->_zendDb->quoteIdentifier($this->_identityColumn, true) . ' = ?', $this->_identity);
return $dbSelect;
}
}
I explained what did it for me in the question.
But, to repeat, easiest for me was to change Zend_Auth_Adapter_DbTable::_authenticateCreateSelect().
There are a method named Zend_Auth_Adapter_DbTable::getDbSelect returns Zend_Db_Select object.
Call it and then you can join those two tables.
Hope this help.
Regards,
Ahmed B.
Here's an alternate method.
Extend the Zend_Auth_Adapter_DbTable class.
class My_Auth_Adapter_DbTable extends Zend_Auth_Adapter_DbTable {
public function setDbSelect($select) {
$this->_dbSelect = $select;
return $this;
}
}
Create instance of your new adapter
$authAdapter = new My_Auth_Adapter_DbTable(
$this->dbTable->getAdapter()
, 'Users'
, 'Users.username'
, 'Users.password'
);
In your Application_Model_DbTable_Users class, create a method that returns the select object with the joined tables.
public function getSelectAuth() {
$select = $this
->select()
->setIntegrityCheck(false)
->from(array('SystemPeopleJoined' => $this->_name)
, array(
'id'
, 'person_id'
, 'system_role_id'
, 'created_on'
, 'expires_on'
)
)
->joinInner(
'People'
, 'People.id = SystemPeopleJoined.person_id'
, array(
'first_name' => 'first_name'
, 'last_name' => 'last_name'
, 'name' => "CONCAT_WS(' ', `People`.`first_name`, `People`.`last_name`)"
)
return $select;
}
Set the select object in your adapter
$select = $this->dbTable->getSelectAuth();
$authAdapter
->setDbSelect($select)
->setIdentity($params['username'])
->setCredential($params['password'])
->setCredentialTreatment("SHA1(?)")
;

Auditlogging functionality for zend db

I have a requirement to implement audit logging functionality in a zend project. The models are created using zend db and the update function is as follows.
public function updateGroup($data,$id)
{
$row = $this->find($id)->current();
// set the row data
$row->name = $data['name'];
$row->description = $data['description'];
$row->updatedBy = $data['updatedBy'];
$row->updatedOn = date('Y-m-d H:i:s');
$id = $row->save();
return $id;
}
I have to create a table with the auditlog information which includes the current userid. I have tried many methods and nothing is a good solution. What is the best practice for a good audit logging functionality for zend?
I just want to log only the modified data. and the log table schema is like
id,
table,
column,
rowId
oldvalue,
newvalue,
updatedon,
updatedbyuser
use Zend_Log_Writer_Db :
Zend_Log_Writer_Db writes log information to a database table using
Zend_Db. The constructor of Zend_Log_Writer_Db receives a
Zend_Db_Adapter instance, a table name, and a mapping of database
columns to event data items
for example :
$columnMapping = array('name' => 'name',
'desc' => 'desc',
'updatedBy' => 'userid',
'updatedOn' => 'date');
$writer = new Zend_Log_Writer_Db($db, 'auditlog_table', $columnMapping);
$logger = new Zend_Log($writer);
$logger->setEventItem('name', $data['name']);
$logger->setEventItem('desc', $data['name']);
$logger->setEventItem('updatedBy',$data['updatedBy']);
$logger->setEventItem('updatedOn',date('Y-m-d H:i:s'));
EDIT : to log only the modified data :
public function logUpdate(array $values)
{
$columnMapping = array('id' => 'id',
'table' => 'table',
'column' => 'column',
'rowId' => 'rowId',
'oldvalue' => 'oldvalue',
'newvalue' => 'newvalue',
'updatedon' => 'updatedon',
'updatedbyuser' => 'updatedbyuser');
$writer = new Zend_Log_Writer_Db($db, 'auditlog_table', $columnMapping);
$logger = new Zend_Log($writer);
$logger->setEventItem('id', $values['id']);
$logger->setEventItem('table', $values['table']);
$logger->setEventItem('column', $values['column']);
$logger->setEventItem('rowId', $values['rowId']);
$logger->setEventItem('oldvalue', $values['oldValue']);
$logger->setEventItem('newValue', $values['newValue']);
$logger->setEventItem('updatedon', $values['updatedon']);
$logger->setEventItem('updatedbyuser', $values['updatedbyuser']);
}
and in updateGroup :
public function updateGroup($data,$id)
{
$row = $this->find($id)->current();
$values = array('table' => $this->name);
$values = array('updatedon' => $data['updatedBy']);
$values = array('updatedbyuser' => date('Y-m-d H:i:s'));
//go through all data to log the modified columns
foreach($data as $key => $value){
//check if modified log the modification
if($row->$key != $value){
$values = array('column' => $key);
$values = array('oldValue' => $row->$key);
$values = array('newValue' => $value);
logUpdate($values);
}
}
// set the row data
$row->name = $data['name'];
$row->description = $data['description'];
$row->updatedBy = $data['updatedBy'];
$row->updatedOn = date('Y-m-d H:i:s');
$id = $row->save();
return $id;
}
Note that its better to implement logging for all your application and seperate logging from update , see this answer for that .

ReferenceOne with MongoDB

I've got a problem with Symfony2.0 BETA3 and MongoDB. I want create a Document, where a Field References to another Class, this might look like this:
namespace test\TestBundle\Document;
use test\TestBundle\Document\Location;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/**
* #ODM\Document(collection="locationcache", repositoryClass="test\TestBundle\Document\LocationCacheRepository")
*/
class LocationCache
{
// many more fields...
/**
* #var Location
* #ODM\ReferenceOne(targetDocument="test\TestBundle\Document\Location")
*/
protected $location;
/**
* #param Location
*/
public function setLocation(Location $location)
{
$this->location = $location;
}
/**
* #return Location
*/
public function getLocation()
{
return $this->location;
}
}
But if I want to find a location by $id like this
class LocationCacheRepository extends DocumentRepository
{
public function findByLocationID(MongoId $locationID)
{
return $this->createQueryBuilder()
->field('location.$id')->equals($locationID)
->sort('year', 'asc')
->sort('month', 'asc')
->getQuery()
->execute();
}
}
I will get this error
No mapping found for field 'location' in class 'test\TestBundle\Document\LocationCache'.
UPDATE
Here is a Document
Array
(
[_id] => 4dd637e706936bbcc0ac012d
[days] => Array
(
[1] => Array
(
[money] => 9
)
[2] => Array
(
[money] => 21
)
[3] => Array
(
[money] => 38
)
[4] => Array
(
[money] => 6
)
[18] => Array
(
[money] => 6
)
[19] => Array
(
[money] => 3
)
[31] => Array
(
[money] => 11
)
)
[location] => Array
(
[$ref] => location
[$id] => 4dd554c91c911a6606000000
[$db] => test
)
[money] => 94
[month] => 1
[year] => 2011
)
I don't know what's the problem with the class. Could please someone help?
Thanks in advance!
Monty
If you want search on any location field you need to use 'embedOne' instead of 'referenceOne'.
ReferenceOne not copy location fields into parent document, it just data like this (i don't remember exactly):
{
refId: '1',
refColl: 'locations',
refDb: 'location_database'
}
But in general if you need query only by location id you just need take a look how location reference looks like in mongodb using mongoshell or some other tool.
So you query will be like this:
->field('location.$refId')->equals($locationID)