I'm designing a new module in Drupal 8. It's a long-term project that won't be going public for a few months at least, so I'm using it as a way to figure out what's new.
In this module, I want to be able to programmatically create nodes. In Drupal 7, I would do this by creating the object, then calling "node_submit" and "node_save".
These functions no longer exist in Drupal 8. Instead, according to the documentation, "Modules and scripts may programmatically submit nodes using the usual form API pattern." I'm at a loss. What does this mean? I've used Form API to create forms in Drupal 7, but I don't get what the docs are saying here.
What I'm looking to do is programmatically create at least one and possibly multiple new nodes, based on information not taken directly from a user-presented form. I need to be able to:
1) Specify the content type
2) Specify the URL path
3) Set any other necessary variables that would previously have been handled by the now-obsolete node_object_prepare()
4) Commit the new node object
I would prefer to be able to do this in an independent, highly abstracted function not tied to a specific block or form.
So what am I missing?
use Drupal\node\Entity\Node;
$node = Node::create(array(
'type' => 'your_content_type',
'title' => 'your title',
'langcode' => 'en',
'uid' => '1',
'status' => 1,
'field_fields' => array(),
));
$node->save();
RE: deprecated entity create
Here is a short example of usage without the deprecated functions. This is particularly helpful for dynamic creation:
//define entity type and bundle
$entity_type="node";
$bundle="article";
//get definition of target entity type
$entity_def = \Drupal::entityManager()->getDefinition($entity_type);
//load up an array for creation
$new_node=array(
//set title
'title' => 'test node',
//set body
'body' => 'this is a test body, can also be set as an array with "value" and "format" as keys I believe',
//use the entity definition to set the appropriate property for the bundle
$entity_def->get('entity_keys')['bundle']=>$bundle
);
$new_post = \Drupal::entityManager()->getStorage($entity_type)->create($new_node);
$new_post->save();
The Drupal 8 version of devel/devel_generate module has a good example of this.
$edit_node = array(
'nid' => NULL,
'type' => $node_type,
'uid' => $users[array_rand($users)],
'revision' => mt_rand(0, 1),
'status' => TRUE,
'promote' => mt_rand(0, 1),
'created' => REQUEST_TIME - mt_rand(0, $results['time_range']),
'langcode' => devel_generate_get_langcode($results),
);
if ($type->has_title) {
// We should not use the random function if the value is not random
if ($results['title_length'] < 2) {
$edit_node['title'] = devel_create_greeking(1, TRUE);
}
else {
$edit_node['title'] = devel_create_greeking(mt_rand(1, $results['title_length']), TRUE);
}
}
else {
$edit_node['title'] = '';
}
// #todo Remove once comment become field. http://drupal.org/node/731724
if (Drupal::moduleHandler()->moduleExists('comment')) {
$edit_node['comment'] = variable_get('comment_' . $node_type, COMMENT_NODE_OPEN);
}
$node = entity_create('node', $edit_node);
Using formatted text
Using grep with before/after code lines helped me figure out how to add a node with 'full_html'.
Search the Drupal core code with this :
$ cd drupal/core
$ grep -B 5 -A 5 -r entity_create.*node * > /tmp/temp-grep.txt
Then, open up /tmp/temp-grep.txt in a text editor. Poke around there a bit and you'll see this :
--
modules/editor/src/Tests/EditorFileUsageTest.php- $body_value .= '<img src="awesome-llama.jpg" data-editor-file-uuid="invalid-editor-file-uuid-value" />';
modules/editor/src/Tests/EditorFileUsageTest.php- // Test handling of a non-existing UUID.
modules/editor/src/Tests/EditorFileUsageTest.php- $body_value .= '<img src="awesome-llama.jpg" data-editor-file-uuid="30aac704-ba2c-40fc-b609-9ed121aa90f4" />';
modules/editor/src/Tests/EditorFileUsageTest.php- // Test editor_entity_insert(): increment.
modules/editor/src/Tests/EditorFileUsageTest.php- $this->createUser();
modules/editor/src/Tests/EditorFileUsageTest.php: $node = entity_create('node', array(
modules/editor/src/Tests/EditorFileUsageTest.php- 'type' => 'page',
modules/editor/src/Tests/EditorFileUsageTest.php- 'title' => 'test',
modules/editor/src/Tests/EditorFileUsageTest.php- 'body' => array(
modules/editor/src/Tests/EditorFileUsageTest.php- 'value' => $body_value,
modules/editor/src/Tests/EditorFileUsageTest.php- 'format' => 'filtered_html',
--
Note how 'body' now becomes an array with a 'value' and a 'format'.
Best way to create node in Drupal 8 via using core services
$node = \Drupal::entityTypeManager()->getStorage('node')->create([
'type' => 'content_type_machine_name',
'field_text' => 'Foo',
'title' => 'Text Title',
]);
$node->save();
Figured it out. For anyone else with this issue, nodes are now treated as entities, and the entity module is now part of core. So my code ended up looking like this:
$new_page_values = array();
$new_page_values['type'] = 'my_content_type';
$new_page_values['title'] = $form_state['values']['page_title'];
$new_page_values['path'] = $new_page_path;
$new_page = entity_create('node', $new_page_values);
$new_page->save();
Related
I am looking to develop a custom logic hook for our SugarCRM system (uses PHP) which provides the following function:
On Save of a Quote record (either new or update):
IF no contact records have already been linked to the quote THEN
Get contacts linked to the related Opportunity
Link these contacts to the quote
I have created the logic hook as per SugarCRM developer documentation and loaded into our dev instance (we run in SugarCloud) but the hook does not appear to fire. Can anyone help me?
./custom/Extension/modules/Quotes/Ext/LogicHooks/initialiseRecord.php
<?php
$hook_version = 1;
$hook_array['after_save'][] = Array(
//Processing index. For sorting the array.
1,
//Label. A string value to identify the hook.
'after_save example',
//The PHP file where your class is located.
'custom/modules/Quotes/initialise_record.php',
//The class the method is in.
'after_save_initialise_class',
//The method to call.
'after_save_initialise'
);
.custom/modules/Quotes/initialise_record.php
<?php
if (!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
// bean in this context will be Quotes bean being saved
class after_save_initialise_class
{
function after_save_initialise($bean, $event, $arguments)
{
require_once('include/SugarQuery/SugarQuery.php');
$sugarQuery = new SugarQuery();
//Determine if contacts have already been linked
if ($bean->load_relationship('contacts')) {
//Fetch related beans
$relatedBeans = $bean->contacts->get();
// If no contacts have already been connected then initialise. Otherwise abort.
if (count($relatedBeans) === 0) {
// Step 1: Get Opportunity related to quote.
if ($bean->load_relationship('opportunities')) {
//Fetch related beans
$relatedOpportunityBeans = $bean->opportunities->get();
//Retrieve the bean id of the linked opportunity record
$opportunity_record_id = $relatedOpportunityBeans[0];
// Step 2: Get Contacts linked to Opportunity
//Retrieve Opportunity bean
$opportunity_bean = BeanFactory::retrieveBean('Opportunities', $opportunity_record_id);
if ($opportunity_bean->load_relationship('contacts')) {
//Retrieve all contacts connected to that opportunity
$relatedContactBeanIds = $bean->contacts->get();
// Loop through these adding them to the quote bean
foreach ($relatedContactBeanIds as &$value) {
// quotes_contacts_1 is the name of custom many-to-many relationship in SugarCRM between the Quotes and Contacts module created in 'Studio' within SugarCRM
$bean->quotes_contacts_1->add($value);
};
};
};
}
}
}
}
.manifest.php {Used by SugarCRM Package Loader to install the package}
<?php
$manifest = array(
'key' => 202106270819,
'name' => 'Sync Contacts To Quote',
'description' => 'Adds Custom Logic Hook to Sync Contacts from Opportunity to Quote Level',
'author' => 'TBC',
'version' => '1.0',
'is_uninstallable' => true,
'type' => 'module',
'acceptable_sugar_versions' =>
array(
'regex_matches' => array(
'11.0.*' //any 11.0 release
),
),
'acceptable_sugar_flavors' =>
array(
'PRO',
'ENT',
'ULT'
),
'readme' => '',
'icon' => '',
'remove_tables' => '',
'uninstall_before_upgrade' => false,
);
$installdefs = array (
'id' => 'SyncContactOpportunityQuote',
'hookdefs' => array(
array(
'from' => '<basepath>/custom/Extension/modules/Quotes/Ext/LogicHooks/initialiseRecord.php',
'to_module' => 'application',
)
),
'copy' => array(
0 => array(
'from' => '<basepath>/custom/modules/Quotes/initialise_record.php',
'to' => 'custom/modules/Quotes/initialise_record.php'
)
)
);
?>
References:
https://support.sugarcrm.com/Documentation/Sugar_Developer/Sugar_Developer_Guide_11.0/Architecture/Logic_Hooks/
https://support.sugarcrm.com/Documentation/Sugar_Developer/Sugar_Developer_Guide_11.0/Data_Framework/Models/SugarBean/#Updating_a_Bean
Does your Hook get executed at all? How did you verify that it does or doesn't?
If it does get loaded, I suspect that it skips/quits at
//Determine if contacts have already been linked
if ($bean->load_relationship('contacts')) {
//Fetch related beans
$relatedBeans = $bean->contacts->get();
contacts in 2nd and last line isn't an actual field in Quotes, is it?
Did you mean to use quotes_contacts_1 in those two places?
(Which you referred to farther below)
Also with regards to the manifest.php:
I'm not familiar with hookdefs (I just use regular copy to install the hook file), but 'to_module' => 'application',, seems odd - don't you mean 'to_module' => 'Quotes',?
To get related contacts from opportunities
$opp = $bean->opportunities->getBeans();
$opportunity_record_id = $opp->id;
I want to write an extension for Parsedown so that I can add a default class to each of the table tags. I've found that I can successfully hack the source code by adding lines to assign attributes in the blockTable function (around line 870):
$Block = array(
'alignments' => $alignments,
'identified' => true,
'element' => array(
'name' => 'table',
'handler' => 'elements',
'attributes' => array(
'class' => 'table',
),
),
);
However, if I try to loosely follow the Change Element Markup extension tutorial I am unsuccessful (perhaps because the table parsing may be an iterative process and the example in the tutorial is a simple string replacement.)
I've tried:
class Extension extends Parsedown
{
protected function blockTable($Line, array $Block = null)
{
$Block = parent::blockTable($Line, array $Block = null);
$Block['table']['attributes']['class'] = 'table';
return $Block;
}
}
but that doesn't work.
I'm not too sure what's wrong with your code, as your code matches mine. I simply added
'attributes' => array(
'class' => 'table table-responsive'
),
to identifyTable, on line 850 so that it became
$Block = array(
'alignments' => $alignments,
'identified' => true,
'element' => array(
'name' => 'table',
'handler' => 'elements',
'attributes' => array(
'class' => 'table table-responsive',
),
),
);
which works just fine for me. But that appears to be the same for you, minus table-responsive.
What version are you using?
I know this is a very old question asked 5 years, 3 month ago, but I came across this answer on a google search, so thought it was worthwhile answering with the code which outputs table class.
class Extension extends Parsedown {
protected function blockTable($Line, ?array $Block = null)
{
$Block = parent::blockTable($Line, $Block);
if(is_null($Block)){ return; }
$Block['element']['attributes']['class'] = 'table table-responsive';
return $Block;
}
I encountered exactly the same problem with the symfony demo application.
Finally it turned out that it wasn't parsedown, because the output was cleaned up by the html-sanitizer.
Allowing the class attribute for tables solved the issue.
For symfony 4 demo app add to config/packages/html_sanitizer.yaml:
html_sanitizer:
#[...]
sanitizers:
default:
# [...]
tags:
table:
allowed_attributes:
- "class"
I have the following situation: Contacts without a first or last name, in fact, they only have a email address.
I can work with these contacts fine, but when I use the listview anywhere (for instance to show all contacts from a company) there now is no way to click through to the contact (normally you would click on the name).
I'm looking for a way to solve this, for instance by showing a clickable text like 'name not known', but can't figure out how to do this. I've been looking at the manual and in the files in the modules directory and the sugarfields dir, but can't quite figure it out.
The closest I got was in /sugarcrm/modules/Contacts/metadata/listviewdefs.php
where this piece of code resides:
$listViewDefs['Contacts'] = array(
'NAME' => array(
'width' => '20%',
'label' => 'LBL_LIST_NAME',
'link' => true,
'contextMenu' => array('objectType' => 'sugarPerson',
'metaData' => array('contact_id' => '{$ID}',
'module' => 'Contacts',
'return_action' => 'ListView',
'contact_name' => '{$FULL_NAME}',
'parent_id' => '{$ACCOUNT_ID}',
'parent_name' => '{$ACCOUNT_NAME}',
'return_module' => 'Contacts',
'return_action' => 'ListView',
'parent_type' => 'Account',
'notes_parent_type' => 'Account')
),
'orderBy' => 'name',
'default' => true,
'related_fields' => array('first_name', 'last_name', 'salutation', 'account_name', 'account_id'),
),
Somewhere there has to be a function that joins the first and lastname together...
Edit: I found a solution:
The actual concatenation function is in /sugarcrm/include/SugarObjects/templates/person/person.php and is called _create_proper_name_field()
I can modify the output for my specific case by adding something like this to the end of the function:
if (empty(trim($full_name))){
$full_name = 'Name unknown';
}
However, I would rather have a upgrade safe solution, so that will be the next challenge.
Don't edit the core because the next upgrade will break your SugarCRM instance. Use logic hooks to be upgrade safe:
create a file 'logic_hooks.php' in /custom/modules/Contacts/
In that file, add the followin code:
<?php
$hook_array['before_save'][] = Array(1,'logic_fill_name','custom/modules/Contacts/logic_hooks/logics.php','ContactLogics','logic_fill_name');
After you have done this. create the file 'logics.php' in /custom/modules/Contacts/logic_hooks.
In the logics.php file, add something like:
<?php
require_once 'include/SugarQuery/SugarQuery.php';
/**
* Class ContactLogics
*/
class ContactLogics {
/**
* #param $bean
* #param $event
* #param $arguments
*/
public function logic_fill_name($bean, $event, $arguments) {
if (empty(trim($bean->first_name)) && empty(trim($bean->last_name))){
$bean->last_name = 'Name unknown';
}
}
}
Now some explanation. When you edited a recordview and pressed the save button, the logic hook 'before_save' will be triggered. This code will change the full name to 'Name unknown' when the full name is empty. When the 'before_save' is executed, the actual save will take place.
I've created a module which defines a new text format filter.
Now I want to define a text format using this new filter, directly from module php. Drupal Administrator can do this manually from admin/config/content/formats/add admin page, but I want to avoid this step. What do I need to add to my_dmodule.module?
I'm guessing that you are trying to create a text format on module installation. If so, you could call filter_format_save(). The details on creating the object can be found in filter.module (Drupal API reference).
You might have to load the filter module first if you are creating the filter in hook_install(), haven't checked:
drupal_load('module', 'filter');
Inspired by Hendrik's answer, this is my solution:
function myformat_install() {
drupal_load('module', 'filter');
/* check already exists */
$format_exists = (bool) db_query_range('SELECT 1 FROM {filter_format} WHERE name = :name', 0, 1, array(':name' => 'My Format'))->fetchField();
if (!$format_exists) {
$format = array(
'format' => 'myformat',
'name' => 'My Format',
'filters' => array(
'myformat_filter' => array(
'weight' => 0,
'status' => 1,
),
),
);
$format = (object) $format;
filter_format_save($format);
}
}
myformat_filter is a filter defined implementing hook_filter_info(), but it could be a filter defined in another module.
So, I am able to upload a video to YouTube (direct upload) using the PHP client library and set it to private, but is it possible to set it as unlisted?
You must use this code as a child of the XML element of the request:
<yt:accessControl action="list" permission="denied"/>
If you can't add it manually (usually using zend) you may use this code to add the corresponding zend entry:
//Creates an extension to Zend Framework
$element = new Zend_Gdata_App_Extension_Element('yt:accessControl', 'yt', 'http://gdata.youtube.com/schemas/2007', '');
//Adds the corresponding XML child/attribute
$element->extensionAttributes = array(array('namespaceUri' => '', 'name' => 'action', 'value' => 'list'), array('namespaceUri' => '', 'name' => 'permission', 'value' => 'denied'));
//Adds this extension to you video entry where "$myVideo" is your video to be uploaded
$myVideo->extensionElements = array($element);
Hope this helps :D
Do that.. with API ver 2 and ZEND GDATA.
If you look at the content of a $videoEntry you will note a
$_extensionElements and $_extensionArributes.
So looking backwards from the extended class of VideoEntry
you will found the abstract class Zend_Gdata_App_Base
and it has a function setExtensionElements(array).
So only do what others say to create the accesControlElement
and pass it to that function..
And IT WORKS.
$videoEntry = $yt->getFullVideoEntry($id);
if ($videoEntry->getEditLink() !== null) {
echo "<b>Video is editable by current user</b><br />";
$putUrl = $videoEntry->getEditLink()->getHref();
//set video to unlisted
$accessControlElement = new Zend_Gdata_App_Extension_Element(
'yt:accessControl', 'yt', 'http://gdata.youtube.com/schemas/2007', ''
);
$accessControlElement->extensionAttributes = array(
array(
'namespaceUri' => '',
'name' => 'action',
'value' => 'list'
),
array(
'namespaceUri' => '',
'name' => 'permission',
'value' => 'denied'
));
// here is the hidden function
// it´s on a abstract class Zend/Gdata/App/Base/Base.php
// Where ZEND/Gdata/Youtube/VideoEntry.php extends
$videoEntry->setExtensionElements(array($accessControlElement));
$yt->updateEntry($videoEntry, $putUrl);
}else{
echo "<b>EL Video no es editable por este usuario</b><br />";
}