Dynamic number of Contact Form 7 recipients - wordpress

I am working a on a form with, at it's core, two fieldset: main and "other recipient"; at the end of of the "other recipient" fieldset, I have a "add another recipient" link.
Here's who needs what:
Main recipient: everything
Other recipient: the "other recipient" fieldset;
Sub-sequent recipients: Respective fieldsets
So far, I've been looking at the Documentation but not much luck there, not that I expected any, either.
Edit
I think this is unclear, so I will be a little more explicit as to what is the context. My form is a registration where we can sign up multiple people; one of the fields is labeled "Your email". Since we can register more than one person at once, I need to duplicate the fieldset containing "Your email".
Edit 2
To help clarify, imagine that we are signing up kids for a summer camp. The first fieldset is general, say the parent's billing information, and the second fieldset is the child's information. The parent needs to be able to fill out a single form and dynamically add as many children as the parent desires.
In each of the children's fieldset, their email is required and they receive the information relevant to this child, where the email would be similar to:
Hello {children's name},
You've been registered to StackOverflow Summer Camp. Here's the information you need to know:
[...]
Thanks for being a good sport!
Hope this helps.

When you've got a specific use case like this, shoehorning the functionality into peripherally related plugins often results in frustration. That being said - there are times where you're married to a specific plugin or approach, and just have to build on top of it.
With that caveat out of the way, I think you should approach this from the angle of creating a new fieldtype for Contact Form 7. This way you have control over rendering the field's HTML, the data validation, among other things. It might also provide an launch point for DB storage and sending reminders, later, as you've mentioned in a comment on another answer.
Here's this approach in action:
The new fieldtype is called emailplus, and you include it into a form like this:
<div class="cf7-duplicable-recipients">
<label>Main Recipient (required)</label>
[emailplus emails]
[submit "Sign Up"]
</div>
Additionally, I've set the recipient under the "mail" panel in the form's settings to [emails].
If an emailsplus field is set as the recipient, the class overrides the default wpcf7 behaviour and sends mail to each value in the email array, substituting any [emails] placeholders in the body of the message on a per-email basis.
The emailplus fieldtype is encapsulated here in a class, and commented liberally:
<?php
class WPCF7_Duplicable_Email
{
/**
* The emails this form's field is addressed to
*
* #var array
*/
public $emails = array();
/**
* The name of the tag bearing the emailplus type
*
* #var string
*/
public $tag_name;
/**
* Instantiate the class & glom onto the wpcf7 hooks
*
* #return void
*/
public function __construct()
{
add_action('wpcf7_init', array($this, 'add_emailplus_tag'));
add_action('wpcf7_form_tag', array($this, 'assign_values_to_field'));
add_filter('wpcf7_validate_emailplus', array($this, 'validate'), 2);
add_action('wpcf7_before_send_mail', array($this, 'send_email'));
add_action('wp_enqueue_scripts', array($this, 'emailplus_scripts'));
}
/**
* Add our the [emailplus __YOUR_FIELD_NAME__] shortcode for use in wpcf7 forms.
*
* #uses wpcf7_add_shortcode
*
* #return void
*/
public function add_emailplus_tag()
{
wpcf7_add_shortcode(
array('emailplus'),
array($this, 'shortcode_handler'),
true
);
}
/**
* Renders the HTML for the [emailplus] shortcode
*
* Referenced from wpcf7_text_shortcode_handler in wp-content/plugins/contact-form-7/modules/text.php
*
* #param array $tag The data relating to the emailplus form field.
*
* #uses wpcf7_get_validation_error
* #uses wpcf7_form_controls_class
*
* #return string
*/
public function shortcode_handler(array $tag) {
$tag = new WPCF7_Shortcode($tag);
if (true === empty($tag->name)) {
return '';
}
$validation_error = wpcf7_get_validation_error($tag->name);
$class = wpcf7_form_controls_class($tag->type);
if ($validation_error) {
$class .= ' wpcf7-not-valid';
}
$atts = array(
'class' => $tag->get_class_option($class),
'id' => $tag->get_id_option(),
'tabindex' => $tag->get_option('tabindex', 'int', true),
'aria-invalid' => $validation_error ? 'true' : 'false',
'type' => 'email',
'name' => $tag->name.'[]', // <-- Important! the trailing [] Tells PHP this is an array of values
'value' => $tag->get_default_option()
);
$atts = wpcf7_format_atts($atts);
$html = sprintf(
'<div class="emailplus-wrapper %1$s"><input %2$s />%3$s</div>',
sanitize_html_class($tag->name),
$atts,
$validation_error
);
// We identify the container that will hold cloned fields with the [data-wpcf7-duplicable-email] attr
return '<div class="wpcf7-form-control-wrap %1$s" data-wpcf7-duplicable-email>'.$html.'</div>';
}
/**
* Validates the value of the emailplus tag.
*
* Must be handled separately from other text-based form inputs,
* since the submitted POST value is an array.
*
* We can safely assume emailplus while creating the WPCF7_Shortcode,
* because we know we hooked this function onto 'wpcf7_validate_emailplus'
*
* #uses wpcf7_is_email
* #uses WPCF7_Validation::invalidate
*
* #param WPCF7_Validation $result The validation helper class from wpcf7.
* #param array $tag The array of values making up our emailplus tag
*
*/
public function validate(WPCF7_Validation $result, $tag)
{
$tag = new WPCF7_Shortcode(
array(
'basename' => 'emailplus',
'name' => $this->tag_name,
'raw_values' => $this->emails
)
);
// Check each value of the form field.
// Emails must be validated individually.
foreach($tag->raw_values as $email) {
if (false === wpcf7_is_email($email)) {
$result->invalidate($tag, wpcf7_get_message('invalid_email'));
}
}
return $result;
}
/**
* For completeness' sake, manually assign the value to the emailplus fieldtype.
*
* Wpcf7 doesn't know how to treat our fieldtype's value by default.
*
* As a side effect, this sets the email addresses that are up for delivery.
*
* #param array $scanned_tag The tag that wpcf7 is scanning through, and processing.
*
* #return $array;
*/
public function assign_values_to_field($scanned_tag)
{
if ($scanned_tag['type'] !== 'emailplus') {
return $scanned_tag;
}
$this->tag_name = $scanned_tag['name'];
if (key_exists($scanned_tag['name'], $_POST)) {
// Stores the emails on a class property for use later.
$this->emails = $_POST[$scanned_tag['name']];
$scanned_tag['raw_values'] = $this->emails;
$scanned_tag['values'] = $this->emails;
}
return $scanned_tag;
}
/**
* Performs a substitution on the emailplus field's fieldname, on a per-value basis.
*
* Ex. in two parts
* 1 - The shortcode [emailsplus emails] renders into <input type="email" name="emails[]" value="" >
* Which the user clones and submits, processing into something like
* ['test1#gmail.com', 'test2#gmail.com'].
* 2 - The user has set [emails] as the recipient in the "mail" panel for the form.
*
* Because wpcf7 isn't aware of how to process a field w/ multiple values when emailing,
* we loop over the values of [emails], replace the tag, and invoke WPCF7_Mail::send()
* for each value.
*
* #param WPCF7_ContactForm $form The contact form object.
*
* #uses WPCF7_Mail::send
*
* #return void
*/
public function send_email(WPCF7_ContactForm $form)
{
$placeholder = '['.$this->tag_name.']';
if (false === strpos($form->prop('mail'), $placeholder)) {
return;
}
foreach ($this->emails as $email) {
$template = $form->prop('mail');
$template['recipient'] = str_replace($placeholder, $email, $template['recipient']);
$template['body'] = str_replace($placeholder, $email, $template['body']);
WPCF7_Mail::send($template);
}
// Tell cf7 to skip over the default sending behaviour in WPCF7_Submission->mail()
$form->skip_mail = true;
}
/**
* Adds our js that will clone the emailplus field.
*
* Could be optimized with a conditional that checks if there is a form with the [emailplus]
* fieldtype somewhere on the page
*
* #return void
*/
public function emailplus_scripts()
{
wp_enqueue_script(
'cf7-duplication',
get_template_directory_uri() . '/js/cf7-duplication.js',
array('jquery'),
'20161006',
true
);
}
}
$wpcf7DuplicableEmail = new WPCF7_Duplicable_Email();
And, the .js file that handles the cloning. It should live in /path/to/your/theme/js/cf7-duplication.js'.
(function($) {
$(document).ready(function() {
var addEmailField = function(inputRow, container) {
inputRow.find('input[type=email]').val('');
var removeButton = $('×')
.click(function(e) {
e.preventDefault();
inputRow.remove();
});
inputRow.append(removeButton);
inputRow.insertAfter(container.find('.emailplus-wrapper').last());
}
$.each($('[data-wpcf7-duplicable-email]'), function(i, el) {
var container = $(el);
var inputRow = container.find('.emailplus-wrapper');
var addButton = $('Add another email');
addButton.click(function(e) {
e.preventDefault();
var newEmailField = inputRow.clone();
addEmailField(newEmailField, container);
});
container.append(addButton);
});
});
})(jQuery);
Last, but not least, if you'd like the form to fadeout when it's valid and the emails have gone out, add this to the "additional settings" panel.
on_sent_ok: "jQuery('.cf7-duplicable-recipients').fadeOut();"
This approach is best for CF7's AJAX submissions. If you want to extend it to handle vanilla POST requests, you could update the shortcode handler to render multiple <input> fields where you need to preserve the value attr on invalid submissions.

Options
1) Under the Mail tap in the setup menu, after you click Mail(2), in the TO: field add this line. Should the parent have less than the number of kids stated, the extra email addresses should do nothing. [email],[email] is the basic format.
[parents-email], [kid-email1], [kid-email2], [kid-email3], [kid-email4], [kid-email5], [kid-email6], [kid-email7]
Option 2) In the TO: field, just place 1 email address such as the parents. and in the Additional Headers: place the code below
CC: [kid-email1], [kid-email2], [kid-email3], [kid-email4], [kid-email5], [kid-email6], [kid-email7]
or
BCC: [kid-email1], [kid-email2], [kid-email3], [kid-email4], [kid-email5], [kid-email6], [kid-email7]
A possible problem that could arise: Many hosts block things like this to prevent spam. If theses do not work, then this is probably the case. You would need to contact your hosting provider about removing the block.

Contact form 7 has only one recipient field, however you can enter multiple email address with comma separated in that field, example "email1#domain.com,email2#domain.com,email3#domain.com".
So for your case, use JavaScript to add multiple duplicate recipient fields dynamically, and finally on form submit write a JavaScript function to concat all the recipient email addresses then keep that in main recipient field and submit the form. Hope you got my point.

Related

Symfony parameters Routes (Easy to answer)

I try to pass my user id in parameter because I wanted to use it in an other function to link them but don't know why that don't work (I think that I do a mistake and will be easy to answer thx :)
return $this->redirect('/profile/new/', array(
'id' => $user->getId(),
));
My receiver :
/**
* Creates a new profile entity.
*
* #Route("/new/{id}", name="profile_new")
*/
public function newProfileAction(Request $request)
{
when I do /profile/new/8 for example it works! But when I click in the button submit that don't redirect with the id ... (of course the routes are good and when I do - it works :
return $this->redirect('/profile/new');
my receiver (when it works) :
/**
* Creates a new profile entity.
*
* #Route("/new", name="profile_new")
*/
public function newProfileAction(Request $request)
{
You should use $this->redirectToRoute('ROUTENAME',[PARAMETERS]) means:
$this->redirectToRoute('profile_new',['id'=>$ID])
If you use $this->redirect('URL') you have to parse the URL so you need to "/profile/new/ID"

How do I check if email submitted via Contact form 7 exists in my database (more than two times)?

When customer submits email via contact form 7,
If more than two times of same email id exists in contact from db. Need to show the error message (Like you are already submitted more than 2 times)
For Example If test#gmail.com is already submitted two times via contact form 7 and contact form 7 db. Not allow to submit again
I have tried this code
add_filter( 'wpcf7_validate', 'email_already_in_db', 10, 2 );
function email_already_in_db ( $result, $tags ) {
// retrieve the posted email
$form = WPCF7_Submission::get_instance();
$email = $form->get_posted_data('your-email');
// if already in database, invalidate
if( email_already_in_database( $email ) )
$result->invalidate('your-email', 'Your email exists in our database');
// return the filtered value
return $result; }
But simple validation of email is also not working, When i use this code in function.php, after submit, loader keep on loading. Not submitting the data.
Check with this plugin ,
https://github.com/mdsimpson/contact-form-7-to-database-extension
After that , please include this script if no inserted ,
https://cfdbplugin.com/?page_id=904
/**
* #param $formName string
* #param $fieldName string
* #param $fieldValue string
* #return bool
*/
function is_already_submitted($formName, $fieldName, $fieldValue) {
require_once(ABSPATH . 'wp-content/plugins/contact-form-7-to-database-extension/CFDBFormIterator.php');
$exp = new CFDBFormIterator();
$atts = array();
$atts['show'] = $fieldName;
$atts['filter'] = "$fieldName=$fieldValue";
$atts['unbuffered'] = 'true';
$exp->export($formName, $atts);
$found = false;
while ($row = $exp->nextRow()) {
$found = true;
}
return $found;
}
/**
* #param $result WPCF7_Validation
* #param $tag array
* #return WPCF7_Validation
*/
function my_validate_email($result, $tag) {
$formName = 'email_form'; // Change to name of the form containing this field
$fieldName = 'email_123'; // Change to your form's unique field name
$errorMessage = 'Email has already been submitted'; // Change to your error message
$name = $tag['name'];
if ($name == $fieldName) {
if (is_already_submitted($formName, $fieldName, $_POST[$name])) {
$result->invalidate($tag, $errorMessage);
}
}
return $result;
}
// use the next line if your field is a **required email** field on your form
add_filter('wpcf7_validate_email*', 'my_validate_email', 10, 2);
// use the next line if your field is an **email** field not required on your form
add_filter('wpcf7_validate_email', 'my_validate_email', 10, 2);
// use the next line if your field is a **required text** field
add_filter('wpcf7_validate_text*', 'my_validate_email', 10, 2);
// use the next line if your field is a **text** field field not required on your form
add_filter('wpcf7_validate_text', 'my_validate_email', 10, 2);
I have a better solution for you that does not require any server interaction. Instead of querying the database each time a user sends an email why not store sent email details locally and then compare them on the client side?
Using JavaScript you can add the information from each sent email to an array kept inside a variable in the sessionStorage or localStorage. Then if that user tries to send another email the content of all previous sent emails will be checked to make sure it's not a duplicate.

How to show content of a custom block using PHP code format in Drupal 8

I have added some custom code in a block using PHP code format to show that block on a specific page. I have checked all the things working fine on Devel PHP page but contents are not showing on page. The code below fetches the field value of a destination node.
$refer = $_SERVER[HTTP_REFERER];
$parsed = parse_url($refer);
$alias = array_pop($parsed);
$dst = \Drupal::service('path.alias_manager')->getPathByAlias($alias , $langcode);
$nid = array_pop(explode('/', $dst));
$dest_node = node_load($nid);
$body = $dest_node->get('body')->getValue();
print $body; //have tried other printing methods also but invain
Hope this clarifies the question.
Thanks
Are you sure that it works in Devel? I've just tried to execute your code, and this line:
$body = $dest_node->get('body')->getValue();
returns Array.
Try to use this one instead:
$body = $dest_node->body->value;
First of all, your first block of code (getting current node) can be replaced with just one line:
$node = \Drupal::service('current_route_match')->getParameter('node');
And the whole block can be changed in the following way:
if ($node = \Drupal::service('current_route_match')->getParameter('node')) {
print $node->body->value;
}
P.S. And it's definitely a bad idea to use PHP text filter. You may easily write your own custom module providing required block. The simplest block plugin requires several lines of code:
/**
* #file
* Contains \Drupal\my_module\Plugin\Block\MyBlock.
*/
namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides my super block.
*
* #Block(
* id = "my_module_block",
* admin_label = #Translation("My Block"),
* category = #Translation("My Module"),
* )
*/
class MyBlock extends BlockBase{
/**
* Builds and returns the renderable array for this block plugin.
*
* #return array
* A renderable array representing the content of the block.
*
* #see \Drupal\block\BlockViewBuilder
*/
public function build() {
if ($node = \Drupal::service('current_route_match')->getParameter('node')) {
return [ '#markup' => $node->body->value ];
}
}
}
This file MyBlock.php must be placed in /src/Plugin/Block/ directory inside your custom module named my_module.

How to correctly create Custom Post Type with custom fields through WP REST API V2

How do I correctly create a custom post type record through the wp rest api v2 with custom fields? Unable to create custom fields or update them.
I have installed the superlist and superlist-child theme in a self hosted wordpress installation.
This theme uses Custom Taxonomies for categories, locations, Custom Post Types called businesses and custom fields in post meta for the Custom Post Type
I am trying to import data to site using rest api v2 and have successfully created the custom categories and locations.
I used the REST API Enabler plugin to register the custom endpoints and the custom fields.
Using the businesses endpoint http://mysite/wp-json/wp/v2/businesses I can upload a new business through the api but the record created ignores all the meta keys
eg Given the following body
body = {
title: company_name,
listing_address: [address],
listing_map_location_latitude: [c.lat],
listing_map_location_longitude: [c.lon],
content: c.description,
email: c.email,
facebook: [c.facebook],
image_url: c.image_url,
locations: [c.city.server_id],
listing_categories: c.categories.pluck(:server_id),
listing_street_view: ["on"]
status:"publish"
}
request = HTTParty.post(POSTS_END_POINT, body: body, headers: headers)
Correctly attaches the business to the correct location and custom categories (listing_categories) previously created through the REST api however none of the custom fields (wp_postmeta table) are populated e.g. listing_locations lon and lat fields and listing_address
I can query the record uploaded through the rest api and I get a response like this
$ rake upload:test
Result: {"id"=>1749, "date"=>"2016-02-20T05:06:01", "date_gmt"=>nil, "guid"=>{"rendered"=>"http://highstreetbeacons.com/?post_type=business&p=1749"}, "modified"=>"2016-02-20T05:06:01", "modified_gmt"=>nil, "slug"=>"", "type"=>"business", "link"=>"http://highstreetbeacons.com/?post_type=business&p=1749", "title"=>{"rendered"=>"Birmingham Museum and Art Gallery, Chamberlain Square, Birmingham, B3 3DH"}, "content"=>{"rendered"=>"<p>A description</p>\n"}, "author"=>1, "featured_media"=>0, "comment_status"=>"open", "ping_status"=>"closed", "listing_categories"=>[3779, 4095, 4061], "locations"=>[330], "_links"=>{"self"=>[{"href"=>"http://highstreetbeacons.com/wp-json/wp/v2/businesses/1749"}], "collection"=>[{"href"=>"http://highstreetbeacons.com/wp-json/wp/v2/businesses"}], "about"=>[{"href"=>"http://highstreetbeacons.com/wp-json/wp/v2/types/business"}], "author"=>[{"embeddable"=>true, "href"=>"http://highstreetbeacons.com/wp-json/wp/v2/users/1"}], "replies"=>[{"embeddable"=>true, "href"=>"http://highstreetbeacons.com/wp-json/wp/v2/comments?post=1749"}], "https://api.w.org/attachment"=>[{"href"=>"http://highstreetbeacons.com/wp-json/wp/v2/media?parent=1749"}], "https://api.w.org/term"=>[{"taxonomy"=>"listing_categories", "embeddable"=>true, "href"=>"http://highstreetbeacons.com/wp-json/wp/v2/listing_categories?post=1749"}, {"taxonomy"=>"locations", "embeddable"=>true, "href"=>"http://highstreetbeacons.com/wp-json/wp/v2/locations?post=1749"}]}}
This totally ignores the meta keys.
If in the admin area I manually hit the update button for the generated business I can then re-query through the api and get all the correct fields returned but an POST request to update (Surely should be a PUT) the record still won't set any of the custom fields
e.g.
rake upload:test
Result: {"id"=>1745, "date"=>"2016-02-20T04:53:17", "date_gmt"=>nil, "guid"=>{"rendered"=>"http://highstreetbeacons.com/?post_type=business&p=1745"}, "modified"=>"2016-02-20T04:53:17", "modified_gmt"=>"2016-02-20T04:53:17", "slug"=>"", "type"=>"business", "link"=>"http://highstreetbeacons.com/?post_type=business&p=1745", "title"=>{"rendered"=>"Birmingham Museum and Art Gallery, Chamberlain Square, Birmingham, B3 3DH"}, "content"=>{"rendered"=>"<p>A description</p>\n"}, "author"=>1, "featured_media"=>0, "comment_status"=>"open", "ping_status"=>"closed", "listing_categories"=>[3779, 4095, 4061], "locations"=>[330], "listing_featured_image"=>[""], "listing_banner"=>["banner_featured_image"], "listing_opening_hours"=>["a:7:{i:0;a:4:{s:11:\"listing_day\";s:6:\"MONDAY\";s:17:\"listing_time_from\";s:0:\"\";s:15:\"listing_time_to\";s:0:\"\";s:14:\"listing_custom\";s:0:\"\";}i:1;a:4:{s:11:\"listing_day\";s:7:\"TUESDAY\";s:17:\"listing_time_from\";s:0:\"\";s:15:\"listing_time_to\";s:0:\"\";s:14:\"listing_custom\";s:0:\"\";}i:2;a:4:{s:11:\"listing_day\";s:9:\"WEDNESDAY\";s:17:\"listing_time_from\";s:0:\"\";s:15:\"listing_time_to\";s:0:\"\";s:14:\"listing_custom\";s:0:\"\";}i:3;a:4:{s:11:\"listing_day\";s:8:\"THURSDAY\";s:17:\"listing_time_from\";s:0:\"\";s:15:\"listing_time_to\";s:0:\"\";s:14:\"listing_custom\";s:0:\"\";}i:4;a:4:{s:11:\"listing_day\";s:6:\"FRIDAY\";s:17:\"listing_time_from\";s:0:\"\";s:15:\"listing_time_to\";s:0:\"\";s:14:\"listing_custom\";s:0:\"\";}i:5;a:4:{s:11:\"listing_day\";s:8:\"SATURDAY\";s:17:\"listing_time_from\";s:0:\"\";s:15:\"listing_time_to\";s:0:\"\";s:14:\"listing_custom\";s:0:\"\";}i:6;a:4:{s:11:\"listing_day\";s:6:\"SUNDAY\";s:17:\"listing_time_from\";s:0:\"\";s:15:\"listing_time_to\";s:0:\"\";s:14:\"listing_custom\";s:0:\"\";}}"], "listing_map_location"=>["a:2:{s:8:\"latitude\";s:0:\"\";s:9:\"longitude\";s:0:\"\";}"], "listing_street_view_location_latitude"=>["37.812405"], "listing_street_view_location_longitude"=>["-122.47607800000003"], "listing_street_view_location_heading"=>["-18"], "listing_street_view_location"=>["a:5:{s:8:\"latitude\";s:9:\"37.812405\";s:9:\"longitude\";s:19:\"-122.47607800000003\";s:4:\"zoom\";s:1:\"1\";s:7:\"heading\";s:3:\"-18\";s:5:\"pitch\";s:2:\"25\";}"], "listing_locations"=>["a:1:{i:0;s:10:\"birmingham\";}"], "listing_listing_category"=>["a:3:{i:0;s:7:\"museums\";i:1;s:10:\"coffee-tea\";i:2;s:13:\"art-galleries\";}"], "_links"=>{"self"=>[{"href"=>"http://highstreetbeacons.com/wp-json/wp/v2/businesses/1745"}], "collection"=>[{"href"=>"http://highstreetbeacons.com/wp-json/wp/v2/businesses"}], "about"=>[{"href"=>"http://highstreetbeacons.com/wp-json/wp/v2/types/business"}], "author"=>[{"embeddable"=>true, "href"=>"http://highstreetbeacons.com/wp-json/wp/v2/users/1"}], "replies"=>[{"embeddable"=>true, "href"=>"http://highstreetbeacons.com/wp-json/wp/v2/comments?post=1745"}], "https://api.w.org/attachment"=>[{"href"=>"http://highstreetbeacons.com/wp-json/wp/v2/media?parent=1745"}], "https://api.w.org/term"=>[{"taxonomy"=>"listing_categories", "embeddable"=>true, "href"=>"http://highstreetbeacons.com/wp-json/wp/v2/listing_categories?post=1745"}, {"taxonomy"=>"locations", "embeddable"=>true, "href"=>"http://highstreetbeacons.com/wp-json/wp/v2/locations?post=1745"}]}}
I have just created a plugin to register the custom fields rather than relying on REST API Enabler plugin with just one field to see if I can get that to work
<?php
/**
* Plugin Name: Jamesc Register API Fields
* Plugin URI: http://danielpataki.com
* Description: This plugin exposes business meta data mfields to REST API.
* Version: 1.0.0
* Author: Smiler
*/
/**
* Use arbitrary functions to add a field
*/
add_action( 'rest_api_init', 'register_something_random' );
function slug_register_something_random() {
register_rest_field( 'business',
'listing_address',
array(
'get_callback' => 'get_meta_field',
'update_callback' => 'update_meta_field',
'schema' => null,
)
);
}
/**
* Handler for getting custom field data.
*
* #since 0.1.0
*
* #param array $object The object from the response
* #param string $field_name Name of field
* #param WP_REST_Request $request Current request
*
* #return mixed
*/
function get_meta_field( $object, $field_name, $request ) {
return get_post_meta( $object[ 'id' ], $field_name );
}
/**
* Handler for updating custom field data.
*
* #since 0.1.0
*
* #param mixed $value The value of the field
* #param object $object The object from the response
* #param string $field_name Name of field
*
* #return bool|int
*/
function update_meta_field( $value, $object, $field_name ) {
if ( ! $value || ! is_string( $value ) ) {
return;
}
return update_post_meta( $object->ID, $field_name, strip_tags( $value ) );
}
But this has no effect.
I am now totally stuck having tried everything I can think of.
There was a glaringly obvious problem with my wordpress plugin where the add action function name did not match the actual function name
register_something_random and slug_register_something_random()
add_action( 'rest_api_init', 'register_something_random' );
function slug_register_something_random() {
Should be
add_action( 'rest_api_init', 'register_something_random' );
function register_something_random() {
Made this change and all is now ok

Update a field after Linking / Unlinking Many-Many records in SilverStripe

I have created a Customer DataObject by extending Member. Customer has a many_many data relation with a Package DataObject.
I would like increment/decrement a Credits field in the Customer DataObject when a Package is linked / unlinked through the CMS based on the Limit field in the Package table.
Customer
class Customer extends Member {
private static $db = array(
'Gender' => 'Varchar(2)',
'DateOfBirth' => 'Date',
'Featured' => 'Boolean',
'Credits' => 'Int'
);
private static $many_many = array(
'Packages' => 'Package'
);
public function getCMSFields() {
$fields = new FieldList();
$config = GridFieldConfig_RelationEditor::create();
$config->removeComponentsByType('GridFieldAddNewButton');
$packageField = new GridField(
'Packages',
'Package',
$this->Packages(),
$config
);
$fields->addFieldToTab('Root.Package', $packageField);
Session::set('SingleID', $this->ID);
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
Package
class Package extends DataObject {
private static $db = array(
'Title' => 'Varchar(255)',
'Limit' => 'Int'
);
private static $belongs_many_many = array(
'Customers' => 'Customer'
);
}
When you create or delete many to many relationship just one record is modified in your database - the one in table which joins elements of both sides of the relationship. Therefore neither object the relationship is based on is updated. This is why methods like: onBeforeWrite, onAfterWrite, onBeforeDelete and onAfterDelete will not be called at all and you cannot use them to detect such change.
However, Silverstripe provides class ManyManyList which is responsible for all operations connected to many to many relationships. There are two methods which are of your interest: add and remove. You can override them and put inside action to do what you need. These methods are obviously called on each link or unlink operation no matter object types are, so you should make some filtering on classes you are particularly interested in.
The proper way to override the ManyManyList class is to use Injector mechanism, so as not to modify anything inside the framework or cms folder. The example below uses relationship between Members and Groups in Silverstripe but you can easily adopt it to your need (Customer -> Member; Package -> Group).
app.yml
Injector:
ManyManyList:
class: ManyManyListExtended
ManyManyListExtended.php
/**
* When adding or removing elements on a many to many relationship
* neither side of the relationship is updated (written or deleted).
* SilverStripe does not provide any built-in actions to get information
* that such event occurs. This is why this class is created.
*
* When it is uses together with SilverStripe Injector mechanism it can provide
* additional actions to run on many-to-many relations (see: class ManyManyList).
*/
class ManyManyListExtended extends ManyManyList {
/**
* Overwritten method for adding new element to many-to-many relationship.
*
* This is called for all many-to-many relationships combinations.
* 'joinTable' field is used to make actions on specific relation only.
*
* #param mixed $item
* #param null $extraFields
* #throws Exception
*/
public function add($item, $extraFields = null) {
parent::add($item, $extraFields);
if ($this->isGroupMembershipChange()) {
$memberID = $this->getMembershipID($item, 'MemberID');
$groupID = $this->getMembershipID($item, 'GroupID');
SS_Log::log("Member ($memberID) added to Group ($groupID)", SS_Log::INFO);
// ... put some additional actions here
}
}
/**
* Overwritten method for removing item from many-to-many relationship.
*
* This is called for all many-to-many relationships combinations.
* 'joinTable' field is used to make actions on specific relation only.
*
* #param DataObject $item
*/
public function remove($item) {
parent::remove($item);
if ($this->isGroupMembershipChange()) {
$memberID = $this->getMembershipID($item, 'MemberID');
$groupID = $this->getMembershipID($item, 'GroupID');
SS_Log::log("Member ($memberID) removed from Group ($groupID)", SS_Log::INFO);
// ... put some additional actions here
}
}
/**
* Check if relationship is of Group-Member type.
*
* #return bool
*/
private function isGroupMembershipChange() {
return $this->getJoinTable() === 'Group_Members';
}
/**
* Get the actual ID for many-to-many relationship part - local or foreign key value.
*
* This works both ways: make action on a Member being element of a Group OR
* make action on a Group being part of a Member.
*
* #param DataObject|int $item
* #param string $keyName
* #return bool|null
*/
private function getMembershipID($item, $keyName) {
if ($this->getLocalKey() === $keyName)
return is_object($item) ? $item->ID : $item;
if ($this->getForeignKey() === $keyName)
return $this->getForeignID();
return false;
}
}
The solution provided by 3dgoo should also work fine but IMO that code does much more "hacking" and that's why it is much less maintainable. It demands more modifications (in both classes) and needs to be multiplied if you would like to do any additional link/unlink managing, like adding custom admin module or some forms.
The problem is when adding or removing items on a many to many relationship neither side of the relationship is written. Therefore onAfterWrite and onBeforeWrite is not called on either object.
I've come across this problem before. The solution I used isn't great but it was the only thing that worked for me.
What we can do is set an ID list of Packages to a session variable when getCMSFields is called. Then when an item is added or removed on the grid field we refresh the CMS panel to call getCMSFields again. We then retrieve the previous list and compare it to the current list. If the lists are different we can do something.
Customer
class Customer extends Member {
// ...
public function getCMSFields() {
// Some JavaScript to reload the panel each time a package is added or removed
Requirements::javascript('/mysite/javascript/cms-customer.js');
// This is the code block that saves the package id list and checks if any changes have been made
if ($this->ID) {
if (Session::get($this->ID . 'CustomerPackages')) {
$initialCustomerPackages = json_decode(Session::get($this->ID . 'CustomerPackages'), true);
$currentCustomerPackages = $this->Packages()->getIDList();
// Check if the package list has changed
if($initialCustomerPackages != $currentCustomerPackages) {
// In here is where you put your code to do what you need
}
}
Session::set($this->ID . 'CustomerPackages', json_encode($this->Packages()->getIDList()));
}
$fields = parent::getCMSFields();
$config = GridFieldConfig_RelationEditor::create();
$config->removeComponentsByType('GridFieldAddNewButton');
$packageField = GridField::create(
'Packages',
'Package',
$this->Packages(),
$config
);
// This class needs to be added so our javascript gets called
$packageField->addExtraClass('refresh-on-reload');
$fields->addFieldToTab('Root.Package', $packageField);
Session::set('SingleID', $this->ID);
$this->extend('updateCMSFields', $fields);
return $fields;
}
}
The if ($this->ID) { ... } code block is where all our session code happens. Also note we add a class to our grid field so our JavaScript refresh works $packageField->addExtraClass('refresh-on-reload');
As mentioned before, we need to add some JavaScript to reload the panel each time a package is added or removed from the list.
cms-customer.js
(function($) {
$.entwine('ss', function($){
$('.ss-gridfield.refresh-on-reload').entwine({
reload: function(e) {
this._super(e);
$('.cms-content').addClass('loading');
$('.cms-container').loadPanel(location.href, null, null, true);
}
});
});
})(jQuery);
Inside the if($initialCustomerPackages != $currentCustomerPackages) { ... } code block there are a number of things you can do.
You could use $this->Packages() to fetch all the current packages associated to this customer.
You could call array_diff and array_merge to get just the packages that have been added and removed:
$changedPackageIDs = array_merge(array_diff($initialCustomerPackages, $currentCustomerPackages), array_diff($currentCustomerPackages, $initialCustomerPackages));
$changedPackages = Package::get()->byIDs($changedPackageIDs);
The above code will add this functionality to the Customer side of the relationship. If you also want to manage the many to many relationship on the Package side of the relationship you will need to add similar code to the Package getCMSFields function.
Hopefully someone can come up with a nicer solution. If not, I hope this works for you.
note: Didn't actually check does the model work but by visually checking this should help you:
On the link you provided you are using
$customer = Customer::get()->Filter...
That returns a DataList of objects, not a singular object unless you specify what is the object you want from the DataList.
If you are filtering the Customers then you want to get a SPECIFIC customer from the DataList, e.g. the first one in this case.
$customer = Customer::get()->filter(array('ID' => $this->CustomerID))->first();
But You should be able to get the singular DataObject with:
$customer = $this->Customer();
As you are defining the Customer as "has_one". If the relation was a Has many, using () would get you a DataList of objects.
Protip:
You don't need to write our own debug files in SilverStripe. It has own functions for it. For example Debug::log("yay"); what writes the output to a file and Debug::dump("yay") that dumps it directly out.
Tip is that you can check what is the object that you accessing right. Debug::dump(get_class($customer)); would output only the class of the object.

Resources