Open main menu

Changes

10,883 bytes added ,  22:33, 13 March 2017
no edit summary
== Examples ==
Just looking for Fatal errors in the log can help you figure out what's wrong.
<source lang="bash">
find /var/www/html/ -regex .*log$ -ls
grep Fatal -A3 /var/www/html/wp-content/uploads/civicrm/ConfigAndLog/CiviCRM.ae1lp|tree|search|stat|r.log | grep --color '\[message\]' -B2
</source>
 
== Debugging CiviCRM on Drupal using NetBeans and XDebug ==
 
This article is a detailed walkthrough of debugging CiviCRM on Drupal (using NetBeans and XDebug). We're specifically debugging the behavior of token replacement and UI representation in the CiviMail component. We've found that if you create 'multi record' custom data that the tokens are not replaced. We're trying to fix that but we're not sure by just reading the code where the problem lies. Hopefully the debugger will provide enough inspection capability to see exactly what's going on; and by extension, what's going wrong. Still haven't found the fix, but it looks like the data (that's there) isn't being picked up in $mailer->compose ([https://github.com/civicrm/civicrm-core/commits/master/api/v3/Mailing.php source])
 
== Call Stacks ==
The overall process of request routing (Drupal menu system), data retrieval, content creation and output is intricate. It gets even more complex when you look at the internals of CiviCRM.
 
Since CiviCRM is a Drupal contrib module, a typical request has a call stack like this:
index.php
 
<pre>
sites/all/modules/contrib/civicrm/CRM/Utils/REST.php.CRM_Utils_REST::ajax:643
sites/all/modules/contrib/civicrm/CRM/Core/Invoke.php.call_user_func:{/var/www/equality-tech.com/www/drupal/sites/all/modules/contrib/civicrm/CRM/Core/Invoke.php:278}:278
sites/all/modules/contrib/civicrm/CRM/Core/Invoke.php.CRM_Core_Invoke::runItem:278
sites/all/modules/contrib/civicrm/CRM/Core/Invoke.php.CRM_Core_Invoke::_invoke:86
sites/all/modules/contrib/civicrm/CRM/Core/Invoke.php.CRM_Core_Invoke::invoke:54
sites/all/modules/contrib/civicrm/drupal/civicrm.module.civicrm_invoke:489
includes/menu.inc.call_user_func_array:{/var/www/equality-tech.com/www/drupal/includes/menu.inc:519}:519
includes/menu.inc.menu_execute_active_handler:519
index.php.{main}:21
</pre>
 
{{highlight|
|text=Pro tip: You can Ctrl+click on all the line items in the Call Stack window of NetBeans. Then press Ctrl+C to copy the text, and paste it into your doc}}
 
Clicking the "Preview as HTML" button from the compose screen of a draft mailing has a call stack like the following. Note that a breakpoint is set at '''line 536''' where <source lang=php inline>$details = CRM_Utils_Token::getTokenDetails($mailingParams, $returnProperties, TRUE, TRUE, NULL, $mailing->getFlattenedTokens());</source>
<pre>
sites/all/modules/contrib/civicrm/api/v3/Mailing.php.civicrm_api3_mailing_preview:536
sites/all/modules/contrib/civicrm/Civi/API/Provider/MagicFunctionProvider.php.Civi\API\Provider\MagicFunctionProvider->invoke:89
sites/all/modules/contrib/civicrm/Civi/API/Kernel.php.Civi\API\Kernel->run:96
sites/all/modules/contrib/civicrm/Civi/API/Subscriber/ChainSubscriber.php.Civi\API\Subscriber\ChainSubscriber->callNestedApi:196
sites/all/modules/contrib/civicrm/Civi/API/Subscriber/ChainSubscriber.php.Civi\API\Subscriber\ChainSubscriber->onApiRespond:72
sites/all/modules/contrib/civicrm/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcher.php.call_user_func:{/var/www/equality-tech.com/www/drupal/sites/all/modules/contrib/civicrm/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcher.php:164}:164
sites/all/modules/contrib/civicrm/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcher.php.Symfony\Component\EventDispatcher\EventDispatcher->doDispatch:164
sites/all/modules/contrib/civicrm/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcher.php.Symfony\Component\EventDispatcher\EventDispatcher->dispatch:53
sites/all/modules/contrib/civicrm/Civi/API/Kernel.php.Civi\API\Kernel->respond:221
sites/all/modules/contrib/civicrm/Civi/API/Kernel.php.Civi\API\Kernel->run:98
sites/all/modules/contrib/civicrm/api/api.php.civicrm_api:23
sites/all/modules/contrib/civicrm/CRM/Utils/REST.php.CRM_Utils_REST::process:404
sites/all/modules/contrib/civicrm/CRM/Utils/REST.php.CRM_Utils_REST::ajax:640
sites/all/modules/contrib/civicrm/CRM/Core/Invoke.php.call_user_func:{/var/www/equality-tech.com/www/drupal/sites/all/modules/contrib/civicrm/CRM/Core/Invoke.php:278}:278
sites/all/modules/contrib/civicrm/CRM/Core/Invoke.php.CRM_Core_Invoke::runItem:278
sites/all/modules/contrib/civicrm/CRM/Core/Invoke.php.CRM_Core_Invoke::_invoke:86
sites/all/modules/contrib/civicrm/CRM/Core/Invoke.php.CRM_Core_Invoke::invoke:54
sites/all/modules/contrib/civicrm/drupal/civicrm.module.civicrm_invoke:489
includes/menu.inc.call_user_func_array:{/var/www/equality-tech.com/www/drupal/includes/menu.inc:519}:519
includes/menu.inc.menu_execute_active_handler:519
</pre>
 
== Tokens Tested ==
{| class="wikitable"
|+ tokens tested
! token !! replaced !! $returnProperties !! multi
|-
| {contact.first_name} || yes || yes || no
|-
| {general.wUrl} || || no || yes
|-
| {custom_40} || || yes || yes
|-
| {contact.custom_40} || || no || yes
|-
| {general.custom_40} || || no || yes
|-
| {general.general.custom_40} || || no || yes
|-
| {contact.custom_71} || || yes* || yes?
|-
| {general.articlepath} || || no || yes
|-
| {general.base} || || no || yes
|-
| {general.favicon} || || no || yes
|-
| {general.logo} || || no || yes
|-
| {general.generator} || || no || yes
|-
| {stats.activeusers} || || no || yes
|-
| {stats.admins} || || no || yes
|-
| {stats.images} || || no || yes
|-
| {stats.pages} || || no || yes
|-
| {stats.wUrl} || || no || yes
|-
| {contact.custom_72} || || ? || no?
|-
| {custom_72} || || yes || no?
|}
 
 
 
There is a lot going on with tokens in CiviCRM. There is a whole file <code>CRM/Utils/Token.php</code> to deal with tokens. 45 methods. 2 properties.
 
It's not just 'getTokens()' that get's called, there is also a function to flattenTokens and getTokenDetails()
 
 
on line 524 of <code>civicrm/api/V3/Mailing.php</code>, in <source lang="php" inline>civicrm_api3_mailing_preview($params) </source> there is a call to <source lang=php inline>CRM_Mailing_BAO_Mailing::tokenReplace($mailing);</source>
When I click "Preview as HTML", the value is correctly displayed for the first contact in my "Recipient list". The token is {contact.custom_71}
I used a breakpoint in <code>civicrm/api/V3/Mailing.php</code> at '''line 536''' where $details gets populated from a call to <source lang=php inline>CRM_Utils_Token::getTokenDetails($mailingParams, $returnProperties, TRUE, TRUE, NULL, $mailing->getFlattenedTokens())</source> $this is a CRM_Mailing_BAO object. $this->tokens is an array[3] with keys html, text, subject $tokens['html'] is an array[4] with keys contact, general, domain, action<ref>this expands to include 'stats' when you use one of those tokens in your message</ref><br />$tokens['html']['contact'] is an array[3] with values 'first_name', 'custom_40' and 'custom_71' <br />$tokens['html']['general'] is an array[2] with values 'wUrl', 'custom_40' <br />$tokens['html']['domain'] is an array[1] with values 'address' <br />$tokens['html']['action'] is an array[1] with values 'optOutUrl' <br />
At that point, the variable $tokens is an <nowiki>array[3]</nowiki> with keys html, text, subject
<nowiki>$tokens['html'] is an array[4] with keys contact, general, domain, action</nowiki><br />
<nowiki>$tokens['html']['contact'] is an array[3] with values 'first_name', 'custom_40' and 'custom_71'</nowiki> <br />
<nowiki>$tokens['html']['general'] is an array[2] with values 'wUrl', 'custom_40'</nowiki> <br />
<nowiki>$tokens['html']['domain'] is an array[1] with values 'address'</nowiki> <br />
<nowiki>$tokens['html']['action'] is an array[1] with values 'optOutUrl'</nowiki> <br />
At this point, $body_html contains the un-evaluated html (meaning the tokens are still present in their literal form).
Interestingly, the $_query is data_select * from $_table civicrm_mailing
<nowiki>$returnProperties is an array[7] with hash, preferred_mail_format, contact_id, display_name, first_name, custom_40, custom_71 all set to (int) 1</nowiki>
Some related constants?
getKeyID is called and for $key "custom_71", it returns "71" since $all is bool 0
<nowiki>Those values populate $cfID and in turn $custom[], so that now $custom is array("40", "71")</nowiki>(and also "72" when a singular value custom field is added - indicating that this is the correct place for this info to be found)
We then go through the autoloader mechanism for "CRM_Contact_BAO_Query"
and $argString is "exportableFields All_1_0_1_2"
Cache::getItem() is called and Array::value() is called on list which looks small at array(15) but elements like <nowiki>[CRM_PC_CRM_Core_DAO_StateProvince_1_id_name_is_active] is array(3907)</nowiki>
Then we go through the DataObject class methods to create a dsn and connection
<source lang="sql">
"SELECT *, config_backend, locales, locale_custom_strings FROM civicrm_domain WHERE ( civicrm_domain.id = 1 ) ";" SELECT subtype.*, parent.name as parent, parent.label as parent_label FROM civicrm_contact_type subtype INNER JOIN civicrm_contact_type parent ON subtype.parent_id = parent.id WHERE subtype.name IS NOT NULL AND subtype.parent_id IS NOT NULL AND subtype.is_active = 1 AND parent.is_active = 1 ORDER BY parent.id";"SELECT * FROM civicrm_mailing_group WHERE ( civicrm_mailing_group.id = 511 )";"SELECT * FROM civicrm_mailing WHERE ( civicrm_mailing.id = 17 ) ";"SELECT * FROM civicrm_domain WHERE ( civicrm_domain.id = 1 ) ";
</source>
{{highlight |text=When stepping through lines of code in NetBeans with the XDebug debugger running, you can mouse over a variable and a tooltip will display it's current value.}}
<nowiki>As CRM_Core_BAO_Cache::getItem() is called, $data gets set, and cached, by unserializing $dao->data. $data is now array[117]</nowiki>
and includes all the 'custom' fields such as
<nowiki>[custom_40] array[10] 'name', 'title', 'headerPattern', 'import', 'custom_filed_id' => (int) 40, 'text_length', 'data_type'=>"String", 'html_type'=>"Text", 'is_search_range'=>"0"
<br /><br />
[custom_71]
self::$_exportableFields[$cacheKeyString] = $fields;
<br /><br />
but a line later, because $status is false, $fields gets set to the stored value of $_exportableFields[$cacheKeyString] BUT inspecting that value in NetBeans before the assignment on line 1609,
<br /><br />
self::$_exportableFields[$cacheKeyString]; says it's type is array[117] but Netbeans only shows it as having 30 elements ????<br /><br />But, that must be some quirk of NetBeans because after the assignment, $fields is indeed an array with 117 elements
<br /><br />
later we get to __construct() in CRM/Contact/BAO/Query.php but $fields is null, so on line 445
</nowiki>
$this->_fields = CRM_Contact_BAO_Contact::exportableFields('All', FALSE, TRUE, TRUE);
NOTICE THE '''5th parameter ($withMultiRecord) is missing''', so the default bool(0) is used. I overrode it and set it to 1
<nowiki>
Later, $this->_fields = array_merge($this->_fields, $fields); array[117], array[122] = array[239]
<br /><br />
<br /><br />
The function name / pattern that they are looking for is eqt_civicrm_queryObjects()
</nowiki>
Somewhere along the line, around here, Symfony\Component\EventDispatcher::dispatch is called and the $event is an Civi\API\Event\RespondEvent object with several properties including the apiRequest with is an array[9] and apiRequest[fields] is array[123] and apiRequests[fields][custom_40] is an array[21] so this is where the API request and response is happening and dispatch gets called.
$contactDetais[2][custom_40] = "https://freephile.org/wiki/Main_Page"
$contactDetais[2][custom_71] = "Not, Never, Well Maybe"
 
So $details '''has the data''' and $contactDetails is a reference to it as well.
 
On '''line 1264''', there is a foreach loop that runs through $custom and assigns values into $contactDetails[$contactID]["custom_{$cfID}"] but those are ALREADY correct, so is this just a formatter? CustomField::getDisplayValue() is called, which calls self::getDisplayValueCommon(). When CustomField::getDisplayValue($value, $id, &$options, $contactID = NULL, $fieldID = NULL) is called, $value is https://freephile.org/wiki/Main_Page, $id is "40", $options is an array of the custom values we care about with an 'attributes' key and I think $contactID was actually null. In any case, it's clear that these are formatter functions and the 'text' html element is returned as is, unless it's an array in which case it's imploded with commas (line 1309 of CustomField).
 
We jump back to "api/v3/Mailing.php" line 538 where $mailing->compose() is called. This is defined in CRM/Mailing/BAO/Mailing.php on line 1175. The function has 12 arguments! It does some odd things with "Prepared Templates" ($pTemplates, $pEmails, $pTemplate, $pEmail, $templates all get copied and referenced many times in loops where it's not clear why the data is being manipulated.) The 8th argument of the compose() method is $contactDetails. When compose() is called from '''line 90''' of CRM/Mailing/Page/Preview.php, the $contactDetails are $details[0][$session->get('userID')] But somehow the reference to $contact in all the getTokenData() calls is a lightweight array of 14 elements - not including things like 'general.base' I'm pretty sure those were in $details[0][2]
 
I then notice that we jumped to MagicFunctionProvider.php where invoke($apiRequest) is called, but only the contact.first_name is replaced correctly in the $result['values']['body_html']
 
on '''line 98''', kernel.php fills in $apiResponse using $this->respond($apiProvider, $apiRequest, $result); but as just noted, $result doesn't have the custom tokens replaced.
 
When EventDispatcher::getListeners() is called, $this->listeners has hook_civicrm_post::Activity and hook_civicrm_post::Case so it's not a place where our custom data hooks are involved.
 
 
 
CRM_Utils_Hook_DrupalBase::CRM_Utils_Hook->invoke() is called which builds the module list and runs the hooks.
Then runHooks() in <code>CRM/Utils/Hook.php</code> is called; and it's called with <source lang=php>$civiModules, $fnSuffix, $numParams, &$arg1, &$arg2, &$arg3, &$arg4, &$arg5, &$arg6</source>
$arg6 is null
</pre>
 
Note: it also gets called with $fnSuffix = "civicrm_apiWrappers" so the hook function would be hook_civicrm_apiWrappers()
 
 
== AJAX ==
The form action is handled by AJAX, and we can see in the $_POST ['entity'] = "Mailing", ['action'] = "create", and ['json'] =
<source lang=javascript wrap>
"{"id":"17","domain_id":"1","footer_id":"2","reply_id":"8","unsubscribe_id":"5","resubscribe_id":"6","optout_id":"7","name":"Test of Custom Data","mailing_type":"standalone","from_name":"Greg Rundlett","from_email":"greg@eQuality-tech.com","replyto_email":"greg@eQuality-tech.com","subject":"test","body_html":"<p>Hi {contact.first_name}<br />\nwUrl data is stored as custom 40<br />\nbut the token in the UI is {general.wUrl}<br />\ncustom_40 {custom_40}<br />\ncontact.custom_40 {contact.custom_40}<br />\ngeneral.custom_40 {general.custom_40}<br />\ngeneral.general.custom_40 {general.general.custom_40}</p>\n\n<p>This is the &quot;dufus&quot; {contact.custom_71}</p>\n\n<p>More tests<br />\n{general.articlepath}<br />\n{general.base}<br />\n{general.favicon}<br />\n{general.logo}<br />\n{general.generator}<br />\n{stats.activeusers}<br />\n{stats.admins}<br />\n{stats.images}<br />\n{stats.pages}</p>\n\n<p></p>\n\n<p>And finally, the Bar test of multiple records</p>\n\n<p>\n{contact.custom_72}<br />\n{custom_72}</p>\n","url_tracking":"1","forward_replies":"0","auto_responder":"0","open_tracking":"1","override_verp":"1","created_id":"2","created_date":"2015-10-06 15:49:42","is_archived":"0","visibility":"Public Pages","dedupe_email":"1","hash":"0e95338dfdcbcc5c","email_selection_method":"automatic","is_error":0,"jobs":{},"scheduled_date":null,"groups":{"include":[17],"exclude":[],"base":[]},"mailings":{"include":[],"exclude":[]},"options":{"force_rollback":1},"api.Mailing.preview":{"id":"$value.id"}}"
</source>
 
== CRM_Utils_Array ==
At some point CRM_Utils_Array::value() gets called on $list, which is array[14] with 'contact_id', 'display_name', 'first_name', 'custom_40', 'custom_71', etc. but it does not have a key for 'general.wUrl' so that lookup fails and NULL is returned (the default).
== Results ==
This is the dufus Not, Never, Well Maybe
</pre>
 
== Resources ==
# https://netbeans.org/kb/docs/php/debugging.html#usingAdditionalWatches
 
 
 
{{References}}
4,558

edits