Search code examples
phpconcrete5concrete5-5.7

Activate a Topic for a Page programmaticaly concrete5 5.7


The goal is to activate an existent topic in a Blog Entry page. Normally a user does this in the Pages Attributes section like so: Page Attributes -> Blog Entry Topics -> activate "Udland"

Now my goal is to do this programmaticaly. I won't post all my trials (since 2 days) here because it's just crap, but here's what I've done so far.

First I add a Blog Page to a chosen parent Page (ID 157):

use Concrete\Core\Page;

$parentPage = Page\Page::getByID(157);
$template = \PageTemplate::getByHandle('blog_entry');
$entry = $parentPage->add($type, array(
    'cName' => 'My title',
    'cDescription' => 'description',
    'cHandle' => 'my_title',
    'cvIsApproved' => true,
    'cDatePublic' => $publishDate->format('Y-m-d H:i:s')
), $template);

As the newly created page is a blog_entry template the Blog Entry Topics is already assigned.

Then I create a Topic and add it to its Topic Tree (Blog Entry Topics) like so:

use \Concrete\Core\Tree\Type\Topic as TopicTree;
use \Concrete\Core\Tree\Node\Type\Topic as TopicTreeNode;
use \Concrete\Core\Tree\Node\Node as TreeNode;

$topicTree = TopicTree::getByName('Blog Entries');
$parentTopic = TreeNode::getByID($topicTree->getRootTreeNodeObject()->treeNodeID);
$item0 = TopicTreeNode::add('udland', $parentTopic);

How to activate/assign this Topic(Udland) to my page ($entry)? (As shown in the image)

I know it must be related to the DB-tables CollectionAttributeValues and atSelectedTopics. Also the Classes CollectionValue and CollectionKey must be involved.

I could add those entries manually in the DB but this isn't a good idea because I don't know what data is necessary to make this work correctly. The topics are used to filter Blog entries so I'm quite sure that there are other tables involved and as a Core developer said: "These are fragile little things" ;-).

As this version of concrete5 is a complete new launch, the developer docs aren't complete and after 2 days of digging inside the core code I'm just desperate.


Update (after a week of digging...)

I managed to do a hack taken out of a Controller method: (/concrete/controllers/panel/page/attributes.php -> submit()).

I know this isn't the way to go at all but it's my best trial so far:

(I just include the NameSpaces here to make clear what Classes I'm calling)

use Concrete\Core\Page;
use Concrete\Core\Page\Collection\Version\Version;
use Concrete\Core\Workflow\Request\ApprovePageRequest;
use CollectionAttributeKey;
use \Concrete\Core\Tree\Node\Type\Topic as TopicTreeNode;

Get the Attributes ID by handle:

$ak = CollectionAttributeKey::getByHandle('blog_entry_topics');
$attributekID = $ak->getAttributeKeyID();

get the topic

$item_one = TopicTreeNode::getNodeByName('Udland');

then simulate a posted form by:

$_POST = array(
    'topics_' . $attributekID => array($item_one->treeNodeID)
);

I know this is so ugly and a big hack & not reliable at all but as said it's taken out of a Controller...

Then I do a slimmed version of the submit() method:

    $c = Page\Page::getByID(157);
    $published = new \DateTime();
    $nvc = $c->getVersionToModify();
    $nvcObj = $nvc->getVersionObject();
    $data = array();
        $data['cName'] = $nvcObj->cvName;
        $data['cDescription'] = $nvcObj->cvDescription;
        $data['cDatePublic'] = $published->format('Y-m-d H:i:s');
        $data['uID'] = '1';

    $nvc->update($data);

    $setAttribs = $nvc->getSetCollectionAttributes();
    $processedAttributes = array();
    $selectedAKIDs = $attributekID;
    if (!is_array($selectedAKIDs)) {
        $selectedAKIDs = array();
    }
    $selected = is_array(array($attributekID)) ? array($attributekID) : array();

    foreach ($setAttribs as $ak) {
            if (in_array($ak->getAttributeKeyID(), $selected)) {
                $ak->saveAttributeForm($nvc);
            } else {
                $nvc->clearAttribute($ak);
            }
            $processedAttributes[] = $ak->getAttributeKeyID();
    }
    $newAttributes = array_diff($selectedAKIDs, $processedAttributes);
    foreach ($newAttributes as $akID) {
            $ak = CollectionAttributeKey::getByID($akID);
            $ak->saveAttributeForm($nvc);
    }

So as said before this is really ugly but it's the best trial so far and somehow it works.

Then approve the Request by doing:

$pkr = new ApprovePageRequest();
$u = new User();
$pkr->setRequestedPage($c);
$v = Version::get($c, "RECENT");
$pkr->setRequestedVersionID($v->getVersionID());
$pkr->setRequesterUserID($u->getUserID());
$pkr->trigger();
$u->unloadCollectionEdit();

But what really makes me wonder is that method inside of /concrete/src/Attribute/Key/Key.php where finally the thing should happen (in my humble opinion):

/**
 * Calls the functions necessary to save this attribute to the database. If no passed value is passed, then we save it via the stock form.
 * NOTE: this code is screwy because all code ever written that EXTENDS this code creates an attribute value object and passes it in, like
 * this code implies. But if you call this code directly it passes the object that you're messing with (Page, User, etc...) in as the $attributeValue
 * object, which is obviously not right. So we're going to do a little procedural if/then checks in this to ensure we're passing the right
 * stuff
 *
 * @param CollectionValue|mixed $mixed
 * @param mixed $passedValue
 */
protected function saveAttribute($mixed, $passedValue = false)
{
    /** @var \Concrete\Core\Attribute\Type $at */
    $at = $this->getAttributeType();

    $at->getController()->setAttributeKey($this);

    if ($mixed instanceof AttributeValue) {
        $attributeValue = $mixed;
    } else {
        // $mixed is ACTUALLY the object that we're setting the attribute against
        //todo: figure out what $nvc should really be since it doesn't exist in this scope
        $attributeValue = $nvc->getAttributeValueObject($mixed, true);
    }
    $at->getController()->setAttributeValue($attributeValue);
    if ($passedValue) {
        $at->getController()->saveValue($passedValue);
    } else {
        $at->getController()->saveForm($at->getController()->post());
    }
    $at->__destruct();
    unset($at);
}

So I'm really curios to see what the reliable and system-suitable way is to resolve this.


Solution

  • Here's what I came up with that does work. You were pretty close.

    use \Concrete\Core\Tree\Type\Topic as TopicTree;
    use \Concrete\Core\Tree\Node\Type\Topic as TopicTreeNode;
    use \Concrete\Core\Tree\Node\Node as TreeNode;
    
    
    $parentPage = \Page::getbyPath('/blog');
    $template = \PageTemplate::getByHandle('blog_entry');
    $entry = $parentPage->add($type, array(
        'cName' => 'ooops',
        'cDescription' => 'hmmmm',
        'cHandle' => 'yay',
        'cvIsApproved' => true,
        'cDatePublic' => '2015-12-21 00:00:00'
    ), $template);
    $item0 = TopicTreeNode::getNodeByName('udland');
    if (!$item0) {
        $topicTree = TopicTree::getByName('Blog Entries');
        $parentTopic = TreeNode::getByID($topicTree->getRootTreeNodeObject()->treeNodeID);
        $item0 = TopicTreeNode::add('udland', $parentTopic);
    }
    
    $entry->setAttribute('blog_entry_topics', array($item0->getTreeNodeDisplayPath()));
    

    It looks like the attribute takes in an array of node display paths and that is how it sets the selection. Additionally, you have to use the \Page alias, and not the fully qualified namespace as you were doing, otherwise you get an error about it being unable to clear the cache.