Security Group subpanel doesn't exist for quotes, contrats, invoices, and events modules - sugarcrm

I am using suiteCRM 7.7.4 (Sugar Version 6.5.24) and I need to use Security group subpanel in quotes, contracts, invoices and events modules, but for some reasons I can't find it ! I did some researches and I found that this subpanel doesn't appear by default for custom modules.. some developers recommand to do not use the studio to build this kind of relationship, because simply it will not work ! for paid version of sugarCRM they say that there was a tool called "hookup tool" that creates the relationship for you... but As I am using a free version I can't use it !
Do you have any idea ?
Thank you very much !

I finaly find a solution :
Adding this few lines to "modules/AOS_Contracts/metadata/subpaneldefs.php" :
'securitygroups' => array(
'top_buttons' => array(array('widget_class' => 'SubPanelTopSelectButton', 'popup_module' => 'SecurityGroups', 'mode' => 'MultiSelect'),),
'order' => 900,
'sort_by' => 'name',
'sort_order' => 'asc',
'module' => 'SecurityGroups',
'refresh_page' => 1,
'subpanel_name' => 'default',
'get_subpanel_data' => 'SecurityGroups',
'add_subpanel_data' => 'securitygroup_id',
'title_key' => 'LBL_SECURITYGROUPS_SUBPANEL_TITLE',
),
QRR
Verifying permissions.

follow this following steps:
1. Go to Admin
2. Go to studio
3. Select your module where you want subpanel like "invoices"
4. Go to relationship
5. Add 1 to many relationship with Security group module.
6. Now repair rebuild you will find the subapnel in invoice module.
When you create 1 to many relationship with any module it will create the subpanel.
IF its not working then go for custom subpanel.
Refer this linnk I put code from same link it worked for me
This tutorial should hopefully help you to create a new subpanel under the Contacts module in Sugar using a custom link class and driven by SugarCRM 7's new SugarQuery API.
Create a new link class
This should go into custom/modules/<YourModule>/YourNewLink.php and this class will act as the custom functionality that will build your link between the two records.
<?php
/**
* Custom filtered link
*/
class YourNewLink extends Link2
{
/**
* DB
*
* #var DBManager
*/
protected $db;
public function __construct($linkName, $bean, $linkDef = false)
{
$this->focus = $bean;
$this->name = $linkName;
$this->db = DBManagerFactory::getInstance();
if (empty($linkDef)) {
$this->def = $bean->field_defs[$linkName];
} else {
$this->def = $linkDef;
}
}
/**
* Returns false if no relationship was found for this link
*
* #return bool
*/
public function loadedSuccesfully()
{
// this link always loads successfully
return true;
}
/**
* #see Link2::getRelatedModuleName()
*/
public function getRelatedModuleName()
{
return '<Your_Module>';
}
/**
*
* #see Link2::buildJoinSugarQuery()
*/
public function buildJoinSugarQuery($sugar_query, $options = array())
{
$joinParams = array('joinType' => isset($options['joinType']) ? $options['joinType'] : 'INNER');
$jta = 'active_other_invites';
if (!empty($options['joinTableAlias'])) {
$jta = $joinParams['alias'] = $options['joinTableAlias'];
}
$sugar_query->joinRaw($this->getCustomJoin($options), $joinParams);
return $sugar_query->join[$jta];
}
/**
* Builds main join subpanel
* #param string $params
* #return string JOIN clause
*/
protected function getCustomJoin($params = array())
{
$bean_id = $this->db->quoted($this->focus->id);
$sql = " INNER JOIN(";
$sql .= "SELECT id FROM accounts WHERE id={$bean_id}"; // This is essentially a select statement that will return a set of ids that you can match with the existing sugar_query
$sql .= ") accounts_result ON accounts_result.id = sugar_query_table.id";
return $sql;
}
}
The argument $sugar_query is a new SugarQuery object, the details of which are documented here. What you essentially need to do is extend this query with whatever join/filters you wish to add. This is done in the inner join I've specified.
Note: The inner join can get really complicated, so if you want a real working example, checkout modules/Emails/ArchivedEmailsLink.php and how the core sugar team use this. I can confirm however that this does work with custom joins.
Here is the getEmailsJoin to help you understand what you can actually produce via this custom join.
/**
* Builds main join for archived emails
* #param string $params
* #return string JOIN clause
*/
protected function getEmailsJoin($params = array())
{
$bean_id = $this->db->quoted($this->focus->id);
if (!empty($params['join_table_alias'])) {
$table_name = $params['join_table_alias'];
} else {
$table_name = 'emails';
}
return "INNER JOIN (\n".
// directly assigned emails
"select eb.email_id, 'direct' source FROM emails_beans eb where eb.bean_module = '{$this->focus->module_dir}'
AND eb.bean_id = $bean_id AND eb.deleted=0\n" .
" UNION ".
// Related by directly by email
"select DISTINCT eear.email_id, 'relate' source from emails_email_addr_rel eear INNER JOIN email_addr_bean_rel eabr
ON eabr.bean_id = $bean_id AND eabr.bean_module = '{$this->focus->module_dir}' AND
eabr.email_address_id = eear.email_address_id and eabr.deleted=0 where eear.deleted=0\n" .
") email_ids ON $table_name.id=email_ids.email_id ";
}
Add a new vardef entry for the link field.
For this example, I'm going to create the custom link on the contacts module. So this code goes in custom/Extension/modules/Contacts/Ext/Vardefs/your_field_name.php
<?php
$dictionary["Contact"]["fields"]["your_field_name"] = array(
'name' => 'active_other_invites',
'type' => 'link',
'link_file' => 'custom/modules/<YourModule>/YourNewLink.php',
'link_class' => 'YourNewLink',
'source' => 'non-db',
'vname' => 'LBL_NEW_LINK',
'module' => '<YourModule>',
'link_type' => 'many',
'relationship' => '',
);
Add the new link as a subpanel
This goes under custom/Extension/modules/Contacts/Ext/clients/base/layouts/subpanels/your_subpanel_name.php
<?php
$viewdefs['Contacts']['base']['layout']['subpanels']['components'][] = array (
'layout' => 'subpanel',
'label' => 'LBL_NEW_LINK',
'context' =>
array (
'link' => 'your_field_name',
),
);
Add the label
Under
custom/Extension/modules/Contacts/Ext/Language/en_us.new_link.php
<?php
$mod_strings['LBL_ACTIVE_OTHER_INVITES'] = 'Your New Link';
Quick Repair and Rebuild
That should hopefully get you started. Keep an eye on the sugarlogs while you're debugging your queries. I also found using xdebug and SugarQueries compileSql function invaluable in figuring out what I needed to do to get a working INNER JOIN statement.
I've found this to be a surprisingly powerful solution, it means that if you need to show information related to a module that might be a few joins away, this allows you to create the links manually without having to create pointless related fields in-between the two.

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 can I refine a REST query based on a linked module?

I'm trying to get all accounts that do not have an associated document.
This is what I have in mind:
$parameters = array(
'session' => $this->getSessionId(),
'module_name' => 'Accounts',
'query' => "accounts.id NOT IN (SELECT DISTINCT account_id FROM documents_accounts)",
);
but of course it doesn't work. I get an "Access denied" error (40).
Ideally I'd like to filter even further by querying for accounts that do not have an attached document of a given type. For example:
getAccountsWithout('contracts');
getAccountsWithout('quote1');
...
It could be that your are not correctly authenticating with the REST API. Or that your are not correctly provide what the method call needs.
If that is not the issue. One solution is to create a customer version the service/v4_1. So that you can add your own custom call.
This can be achieved by copying the v4_1 directory giving it a name like v4_1_1.
Rename the following:
SugarWebServiceImplv4_1.php to SugarWebServiceImplv4_1_1.php
SugarWebServiceUtilv4_1.php to SugarWebServiceImplv4_1_1.php
Change the class signatures to this:
SugarWebServiceImplv4_1:
class SugarWebServiceImplv4_1_1 extends SugarWebServiceImplv4_1
SugarWebServiceImplv4_1:
class SugarWebServiceUtilv4_1_1 extends SugarWebServiceUtilv4_1
Change the require/include paths in:
registry.php rest.php
soap.php
SugarWebServiceImplv4_1_1.php
SugarWebServiceImplv4_1_1.php
so that they now point to the 4_1_1 directory.
Implement a method in SugarWebServiceImplv4_1_1.php.
function get_without_documents($session, $module_name = 'accounts', $start = 1, $offset = 20) {
$output_list = array();
$db = DBManagerFactory::getInstance();
// just for example
$sql_query = "SELECT a.id FROM accounts a WHERE a.id NOT IN (SELECT DISTINCT account_id FROM documents_accounts)";
$result = $db->limitQuery($sql_query, $start, $offset,false);
while(($row = $db->fetchByAssoc($result)) != null){
$output_list[] = $row['id'];
}
return array(
'result_count'=> count($output_list),
'next_offset' => $offset + $start,
'entry_list' => $output_list,
);
}
Finally register the method and it the types in registry.php and the reference the url so that it uses v4_1_1.

Query with orderings causing empty result - Extbase 6.2

I made an extbase extension and want to list my appointments ordered first by startDate and for those appointments that are on the same day I want to order them by the last name of the customer.
In my repository I made following working query:
public function findAppointmentsForList($future) {
$curtime = time();
$query = $this->createQuery();
$constraints = array();
if ($future !== NULL) {
$constraints[] = ($future) ?
$query->greaterThanOrEqual('startDate', $curtime) :
$query->lessThan('startDate', $curtime);
}
if ($constraints) {
$query->matching($query->logicalAnd($constraints));
} else {}
$orderings = array(
'startDate' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
// 'customer.lastName' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING
);
$query->setOrderings($orderings);
return $query->execute();
}
It returns me some appointments so I assume it's working.
If I then uncomment the line 'customer.lastName... it returns 0 appointments.
What's going on? It's just the ordering, it can't possibly make the query smaller...
I don't even get any errors - I tried it with an invalid property for example and it gave me an error, so the property name is correct too.
And I debugged the working query and the last names in those customer objects where there.
This is my appointment model entry:
/**
* customer
*
* #var \vendor\extension\Domain\Model\Customer
*/
protected $customer = NULL;
And this is the TCA corresponding to it:
'customer' => array(
'exclude' => 1,
'label' => 'LLL:EXT:extension/Resources/Private/Language/locallang_db.xlf:tx_extension_domain_model_appointment.customer',
'config' => array(
'type' => 'select',
'foreign_table' => 'fe_users',
'minitems' => 0,
'maxitems' => 1,
),
),
EDIT: It's working now...but unfortunately I don't know why, changed too much in the meantime which I thought had nothing to do with this. One thing that might've affected this: startDate is of type Date and I noticed the query didn't filter it right so after I changed the curtime to new \DateTime('midnight') it was filtering correctly.
When you use related models in a query as part of the matching or orderBy then the query will be build with joins to the related tables. That means, that the result is actually smaller and will not include appointments without customers.
The strange thing is that you see the last names when you debug it, otherwise i would assume that you have some configuration errors in the TCA. Can you provide the TCA code from the apointment table and may be its model?

Zend Framework Router Hostname and Multi-Language support

Last two days I was fighting with Zend Framework Router and still didn't find solution.
Existing project has 2 different modules which work with the same domain e.g. www.domain1.com:
default - Contains public information, can be accessed via www.domain1.com
admin - Administrator interface, can be accessed via www.domain1.com/admin
Project is multi-langual and to keep language code it is transmitted as first parameter of every URL, e.g. www.domain1.com/en/ www.domain1.com/en/admin
Part of code which takes care is next plugin:
class Foo_Plugin_Language extends Zend_Controller_Plugin_Abstract
{
const LANGUAGE_KEY = 'lang';
public function routeStartup(Zend_Controller_Request_Abstract $request)
{
$languagesRegexp = implode('|', array_map('preg_quote', $this->_bootstrap->getLanguages()));
$routeLang = new Zend_Controller_Router_Route(
':' . self::LANGUAGE_KEY,
array(self::LANGUAGE_KEY => $this->_bootstrap->getLanguage()->toString()),
array(self::LANGUAGE_KEY => $languagesRegexp)
);
$router = $this->getFrontController()->getRouter();
$router->addDefaultRoutes();
$chainSeparator = '/';
foreach ($router->getRoutes() as $name => $route) {
$chain = new Zend_Controller_Router_Route_Chain();
$chain
->chain($routeLang, $chainSeparator)
->chain($route, $chainSeparator);
$new_name = $this->_formatLanguageRoute($name);
$router->addRoute($new_name, $chain);
}
protected function _formatLanguageRoute($name)
{
$suffix = '_' . self::LANGUAGE_KEY;
if (substr($name, -strlen($suffix)) == $suffix) return $name;
return $name . '_' . self::LANGUAGE_KEY;
}
public function routeShutdown(Zend_Controller_Request_Abstract $request)
{
$lang = $request->getParam(self::LANGUAGE_KEY, null);
$this->_bootstrap->setLanguage($lang);
$actual_lang = $this->_bootstrap->getLanguage()->toString();
$router = $this->getFrontController()->getRouter();
$router->setGlobalParam(self::LANGUAGE_KEY, $lang);
// Do not redirect image resize requests OR get js, css files
if (preg_match('/.*\.(jpg|jpeg|gif|png|bmp|js|css)$/i', $request->getPathInfo())) {
return true;
}
// redirect to appropriate language
if ($lang != $actual_lang) {
$redirector = Zend_Controller_Action_HelperBroker::getStaticHelper('redirector');
$params = array(self::LANGUAGE_KEY => $actual_lang);
$route = $this->_formatLanguageRoute($router->getCurrentRouteName());
return $redirector->gotoRouteAndExit($params, $route, false);
}
}
}
One of the first question is what do you think about such way to provide multi-lang support. What I've noticed is that all this chains dramatically decrease operation speed of the server, response time from the server is about 4 seconds...
Main question is: Currently I have to implement such feature: I have domain www.domain2.com that should work just with single module e.g. "foobar"... and it should be available via second url... or, of course, it should work like www.domain1.com/en/foobar by default...
To provide this functionality in Bootstrap class I'be implemented such part of code
// Instantiate default module route
$routeDefault = new Zend_Controller_Router_Route_Module(
array(),
$front->getDispatcher(),
$front->getRequest()
);
$foobarHostname = new Zend_Controller_Router_Route_Hostname(
'www.domain2.com',
array(
'module' => 'foobar'
)
);
$router->addRoute("foobarHostname", $foobarHostname->chain($routeDefault));
And that is not working and as I've found routeDefault always rewrite found correct model name "foobar" with value "default"
Then I've implemented default router like this:
new Zend_Controller_Router_Route(
':controller/:action/*',
array(
'controller' => 'index',
'action' => 'index'
);
);
But that still didn't work, and started work without language only when I comment "routeStartup" method in Foo_Plugin_Language BUT I need language support, I've played a lot with all possible combinations of code and in the end made this to provide language support by default:
class Project_Controller_Router_Route extends Zend_Controller_Router_Route_Module
{
/**
* #param string $path
* #param bool $partial
* #return array
*/
public function match($path, $partial = false)
{
$result = array();
$languageRegexp = '%^\/([a-z]{2})(/.*)%i';
if (preg_match($languageRegexp, $path, $matches)) {
$result['lang'] = $matches[1];
$path = $matches[2];
}
$parentMatch = parent::match($path);
if (is_array($parentMatch)) {
$result = array_merge($result, $parentMatch);
}
return $result;
}
}
So language parameter was carefully extracted from path and regular processed left part as usual...
But when I did next code I was not able to get access to the foobar module via www.domain2.com url, because of module name in request was always "default"
$front = Zend_Controller_Front::getInstance();
/** #var Zend_Controller_Router_Rewrite $router */
$router = $front->getRouter();
$dispatcher = $front->getDispatcher();
$request = $front->getRequest();
$routerDefault = new Project_Controller_Router_Route(array(), $dispatcher, $request);
$router->removeDefaultRoutes();
$router->addRoute('default', $routerDefault);
$foobarHostname = new Zend_Controller_Router_Route_Hostname(
'www.domain2.com',
array(
'module' => 'foobar'
)
);
$router->addRoute("foobar", $foobarHostname->chain($routerDefault));
Instead of summary:
Problem is that I should implement feature that will provide access for the secondary domain to the specific module of ZendFramework, and I should save multi-language support. And I cannot find a way, how to manage all of this...
Secondary question is about performance of chain router, it makes site work very-very slow...
The way I have solved problem with multilanguage page is in this thread:
Working with multilanguage routers in Zend (last post).
Ofcourse my sollution need some caching to do, but I think it will solve your problem.
Cheers.

Yii CJuiAutoComplete for Multiple values

I am a Yii Beginner and I am currently working on a Tagging system where I have 3 tables:
Issue (id,content,create_d,...etc)
Tag (id,tag)
Issue_tag_map (id,tag_id_fk,issue_id_fk)
In my /Views/Issue/_form I have added a MultiComplete Extension to retrieve multiple tag ids and labels,
I have used an afterSave function in order to directly store the Issue_id and the autocompleted Tag_ids in the Issue_tag_map table, where it is a HAS_MANY relation.
Unfortunately Nothing is being returned.
I wondered if there might be a way to store the autocompleted Tag_ids in a temporary attribute and then pass it to the model's afterSave() function.
I have been searching for a while, and this has been driving me crazy because I feel I have missed a very simple step!
Any Help or advices of any kind are deeply appreciated!
MultiComplete in Views/Issue/_form:
<?php
echo $form->labelEx($model, 'Tag');
$this->widget('application.extension.MultiComplete', array(
'model' => $model,
'attribute' => '', //Was thinking of creating a temporary here
'name' => 'tag_autocomplete',
'splitter' => ',',
'sourceUrl' => $this->createUrl('Issue/tagAutoComplete'),
// Controller/Action path for action we created in step 4.
// additional javascript options for the autocomplete plugin
'options' => array(
'minLength' => '2',
),
'htmlOptions' => array(
'style' => 'height:20px;',
),
));
echo $form->error($model, 'issue_comment_id_fk');
?>
AfterSave in /model/Issue:
protected function afterSave() {
parent::afterSave();
$issue_id = Yii::app()->db->getLastInsertID();
$tag; //here I would explode the attribute retrieved by the view form
// an SQL with two placeholders ":issue_id" and ":tag_id"
if (is_array($tag))
foreach ($tag as $tag_id) {
$sql = "INSERT INTO issue_tag_map (issue_id_fk, tag_id_fk)VALUES(:issue_id,:tag_id)";
$command = Yii::app()->db->createCommand($sql);
// replace the placeholder ":issue_id" with the actual issue value
$command->bindValue(":issue_id", $issue_id, PDO::PARAM_STR);
// replace the placeholder ":tag_id" with the actual tag_id value
$command->bindValue(":tag_id", $tag_id, PDO::PARAM_STR);
$command->execute();
}
}
And this is the Auto Complete sourceUrl in the Issue model for populating the tags:
public static function tagAutoComplete($name = '') {
$sql = 'SELECT id ,tag AS label FROM tag WHERE tag LIKE :tag';
$name = $name . '%';
return Yii::app()->db->createCommand($sql)->queryAll(true, array(':tag' => $name));
actionTagAutoComplete in /controllers/IssueController:
// This function will echo a JSON object
// of this format:
// [{id:id, name: 'name'}]
function actionTagAutocomplete() {
$term = trim($_GET['term']);
if ($term != '') {
$tags = issue::tagAutoComplete($term);
echo CJSON::encode($tags);
Yii::app()->end();
}
}
EDIT
Widget in form:
<div class="row" id="checks" >
<?php
echo $form->labelEx($model, 'company',array('title'=>'File Company Distrubution; Companies can be edited by Admins'));
?>
<?php
$this->widget('application.extension.MultiComplete', array(
'model' => $model,
'attribute' => 'company',
'splitter' => ',',
'name' => 'company_autocomplete',
'sourceUrl' => $this->createUrl('becomEn/CompanyAutocomplete'),
'options' => array(
'minLength' => '1',
),
'htmlOptions' => array(
'style' => 'height:20px;',
'size' => '45',
),
));
echo $form->error($model, 'company');
?>
</div>
Update function:
$model = $this->loadModel($id);
.....
if (isset($_POST['News'])) {
$model->attributes = $_POST['News'];
$model->companies = $this->getRecordsFromAutocompleteString($_POST['News']
['company']);
......
......
getRecordsFromAutocompleteString():
public static cordsFromAutocompleteString($string) {
$string = trim($string);
$stringArray = explode(", ", $string);
$stringArray[count($stringArray) - 1] = str_replace(",", "", $stringArray[count($stringArray) - 1]);
$criteria = new CDbCriteria();
$criteria->select = 'id';
$criteria->condition = 'company =:company';
$companies = array();
foreach ($stringArray as $company) {
$criteria->params = array(':company' => $company);
$companies[] = Company::model()->find($criteria);
}
return $companies;
}
UPDATE
since the "value" porperty is not implemented properly in this extension I referred to extending this function to the model:
public function afterFind() {
//tag is the attribute used in form
$this->tag = $this->getAllTagNames();
parent::afterFind();
}
You should have a relation between Issue and Tags defined in both Issue and Tag models ( should be a many_many relation).
So in IssueController when you send the data to create or update the model Issue, you'll get the related tags (in my case I get a string like 'bug, problem, ...').
Then you need to parse this string in your controller, get the corresponding models and assigned them to the related tags.
Here's a generic example:
//In the controller's method where you add/update the record
$issue->tags = getRecordsFromAutocompleteString($_POST['autocompleteAttribute'], 'Tag', 'tag');
Here the method I'm calling:
//parse your string ang fetch the related models
public static function getRecordsFromAutocompleteString($string, $model, $field)
{
$string = trim($string);
$stringArray = explode(", ", $string);
$stringArray[count($stringArray) - 1] = str_replace(",", "", $stringArray[count($stringArray) - 1]);
return CActiveRecord::model($model)->findAllByAttributes(array($field => $stringArray));
}
So now your $issue->tags is an array containing all the related Tags object.
In your afterSave method you'll be able to do:
protected function afterSave() {
parent::afterSave();
//$issue_id = Yii::app()->db->getLastInsertID(); Don't need it, yii is already doing it
foreach ($this->tags as $tag) {
$sql = "INSERT INTO issue_tag_map (issue_id_fk, tag_id_fk)VALUES(:issue_id,:tag_id)";
$command = Yii::app()->db->createCommand($sql);
$command->bindValue(":issue_id", $this->id, PDO::PARAM_INT);
$command->bindValue(":tag_id", $tag->id, PDO::PARAM_INT);
$command->execute();
}
}
Now the above code is a basic solution. I encourage you to use activerecord-relation-behavior's extension to save the related model.
Using this extension you won't have to define anything in the afterSave method, you'll simply have to do:
$issue->tags = getRecordsFromAutocompleteString($_POST['autocompleteAttribute'], 'Tag', 'tag');
$issue->save(); // all the related models are saved by the extension, no afterSave defined!
Then you can optimize the script by fetching the id along with the tag in your autocomplete and store the selected id's in a Json array. This way you won't have to perform the sql query getRecordsFromAutocompleteString to obtain the ids. With the extension mentioned above you'll be able to do:
$issue->tags = CJSON::Decode($_POST['idTags']);//will obtain array(1, 13, ...)
$issue->save(); // all the related models are saved by the extension, the extension is handling both models and array for the relation!
Edit:
If you want to fill the autocomplete field you could define the following function:
public static function appendModelstoString($models, $fieldName)
{
$list = array();
foreach($models as $model)
{
$list[] = $model->$fieldName;
}
return implode(', ', $list);
}
You give the name of the field (in your case tag) and the list of related models and it will generate the appropriate string. Then you pass the string to the view and put it as the default value of your autocomplete field.
Answer to your edit:
In your controller you say that the companies of this model are the one that you added from the Autocomplete form:
$model->companies = $this->getRecordsFromAutocompleteString($_POST['News']
['company']);
So if the related company is not in the form it won't be saved as a related model.
You have 2 solutions:
Each time you put the already existing related model in you autocomplete field in the form before displaying it so they will be saved again as a related model and it won't disapear from the related models
$this->widget('application.extensions.multicomplete.MultiComplete', array(
'name' => 'people',
'value' => (isset($people))?$people:'',
'sourceUrl' => array('searchAutocompletePeople'),
));
In your controller before calling the getRecordsFromAutocompleteString you add the already existing models of the model.
$model->companies = array_merge(
$model->companies,
$this->getRecordsFromAutocompleteString($_POST['News']['company'])
);