Jump to content

Understanding FieldtypeTextareaLanguage::wakeupValue


bytesource
 Share

Recommended Posts

Hi,
 
I am am working on a little module that converts markup such as =(123:link text) to the respective hyperlink. 123 in the example is the page ID.
 
The following code was mainly taken from Ryan's Page Link Abstractor, with only a few changes to the regex. I am also hooking into FieldtypeTextareaLanguage::wakeupValue as this is the field type I use for all textareas:
class HyperlinkMarkup extends WireData implements Module {
   
    public static function getModuleInfo() {
        return array(
            'title' => 'Hyperlink Markup',
            'version' => 1,
            'summary' => "Adds Hyperlinks based on the ID of a page using special markup \n
                          Example: =(123:link text)"
        );
    }

    /**
    * Initialize the module
    *
    */
    public function init() {
        $this->addHookBefore('FieldtypeTextarea::wakeupValue', $this, 'wakeupValue');
        $this->addHookBefore('FieldtypeTextareaLanguage::wakeupValue', $this, 'wakeupValue');
    }

    /**
    * Hook into FieldtypeTextarea::wakeupValue to convert abstract URL representations to actual URLs
    *
    * Page URLs are represented by =(123:link text) where "123" is the Page's ID.
    *
    */
    public function wakeupValue($event) {

        // Adapted wakeupValue() from the Page Link Abstractor module:
        // https://github.com/ryancramerdesign/PW2-PageLinkAbstractor/blob/master/PageLinkAbstractor.module#L104
       
        $arguments = $event->arguments; // // $page, $field, $value
        $value = $arguments[2];
        if(empty($value)) return;

        $changes = 0;
        $errors = array();

        /**
        * Hook into FieldtypeTextarea::wakeupValue to convert abstract URL representations to actual URLs
        *
        * Page URLs are represented by =(123:link text) where "123" is the Page's ID.
        *
        */
        if(strpos($value, '=(') !== false) {

            if(preg_match_all('/=\((\d+)[^)]+)\)/', $value, $matches)) {

                // $matches[0] => whole string
                // $matches[1] => first bracket match
                // $matches[2] => second bracket match

                foreach($matches[1] as $key => $id) {

                    $p             = $this->pages->get((int) $id);

                    // Link to non-existend page
                    if (!$p->id) {

                        $errors[] = "links to page ID $id that does not exist";
                        continue;
                    }

                    // Link to page in trash
                    if ($p->isTrash()) {

                        $errors[] = "links to page ID $id that is in the trash";
                        continue;
                    }

                    $url         = $p->url;
                    $linktext     = $matches[2][$key];
                    $hyperlink     = "<a href='$url'>$linktext</a>";

                    $value = str_replace($matches[0][$key], $hyperlink, $value);

                    $changes++;

                }

                if ($changes) {

                    $arguments[2] = $value;
                    $event->arguments = $arguments;
                }

                if (count($errors)) {

                    foreach($errors as $error) $this->error("Page {$page->path} $error");
                }

            }
        }
    }
}
The problem is that this module does not seem to get called. For example, inserting an echo statement at the beginning of wakeupValue() doesn't get output.
 
I already implemented the module as a Textformatter and this one is working, so I assume wakeupValue() might not be the correct function to hook into here.
 
The reason I am also working on this implementation, is because I want to take advantage of the function $this->error(), and I think this not available with a Textformatter.
 
So it would be great if anybody could give me a hint as to how to get this module working.
 

Cheers,

Stefan

Link to comment
Share on other sites

@Wanze

autoload => true did the trick! Thank a lot for this suggestion.

However, now I get the following warning:

strpos() expects parameter 1 to be string, array given

In fact, var_dump($value) shows that $value is an array and not a string as I expected:
 

object(LanguagesPageFieldValue)#344 (8) { ["data":protected]=> array(2) { [1008]=> string(277) some content" [1284]=> string(0) "" } ["defaultLanguagePageID":protected]=> int(1008) ["field":protected]=> object(Field)#177 (9) { ["settings":protected]=> array(5) { ["id"]=> int(100) ["type"]=> object(FieldtypeTextareaLanguage)#140 (7) { ["allowTextFormatters":"FieldtypeText":private]=> bool(true) ["data":protected]=> array(2) { ["inputfieldClass"]=> string(18) "InputfieldTextarea" ["contentType"]=> int(0) } ["useFuel":protected]=> bool(true)

[...]

But most of all I realized that the HTML links this module produces get inserted back into the textarea field, instead of just being output on page load. What I want to accomplish is to dynamically transform the link markup into HTML links without altering the original markup. Otherwise the advantages of using the page ID for identification of a page would get lost.

Is there another method I could hook into, or should I just implement this functionality by extending Textformatter? But then again I like the ability of showing error messages right in the admin if a page does not exists or is in the trash, and if I am not mistaken this is not possible when using a text formatter.

Cheers,

Stefan

Link to comment
Share on other sites

If you change the stored value of the textfield, it is of course loaded this way the next time in the backend. Dynamic transformation is done by textformatters. To accomplish your needs you could implement the evaluation and the replacement seperately. The textformatter for replacement and a hook into page::save() to check for missing / trashed IDs. 

  • Like 1
Link to comment
Share on other sites

@LostKobrakai

I am often not sure which method to hook into as I don't know which methods are called - and in which order - before saving a page or when generating a page's output. If this kind of interaction between methods has been described somewhere before, I would be glad to hear about it!

As for placing the error handling in a different module, this is a good idea as I already made a textformatter transforming link markup into HTML links. Of course, I'd still prefer to have both implementations - the transformation and error handling - inside a single module.

@host

To be honest, I haven't paid much attention to the Hanna Code module before, but looking at the module description now I see whole lot of possibilities. You are right, using Hanna Code to add the link transformation functionality would have been a good choice. 

I understand that Hanna Code is used as a textformatter, so I still would need to implement the error handling in a separate module.

Cheers,

Stefan

Link to comment
Share on other sites

Sadly there's no documentation about the progression of functions there's only this http://processwire.com/api/hooks/captain-hook/ to find hookable functions. But often the hooks are quite declarative. You want your error handling to happen when a page is saved => Page::save. Another example, when a new Page is added to the tree => Pages::add and so on. Your example of Page::render or InputfieldTextarea::render does not make sense for error handling, as this functions only generate markup. Also it's called if someone opens the page, to late for useful error handling, as the user has to reopen the page, before getting the errors.

  • Like 2
Link to comment
Share on other sites

I am often not sure which method to hook into as I don't know which methods are called - and in which order - before saving a page or when generating a page's output. If this kind of interaction between methods has been described somewhere before, I would be glad to hear about it!

 
 
Save process: 
  1. $page->fieldName = 'value'; sends 'value' to Fieldtype::sanitizeValue(); and then sanitized value is kept with $page. 
  2. $pages->save($page); calls Fieldtype::savePageField() for each field on the page that changed. Numbers 3 and 4 below repeat for each of these calls.
  3. Fieldtype::savePageField() calls Fieldtype::sleepValue() to return value as basic type for storage. If value is something simple like an integer or string, then sleepValue may not need to do anything at all. 
  4. Fieldtype::savePageField() saves the "sleeping" value to DB. 
Load process:
  1. Access to $page->fieldName (i.e. $page->title) calls Page::getFieldValue(). 
  2. Page::getFieldValue() calls Fieldtype::loadPageField() to load value from DB (if not previously loaded). 
  3. After Fieldtype::loadPageField() loads value from DB, it calls Fieldtype::wakeupValue() to convert value from basic storage type to runtime type (i.e. array to object). If value is something simple like an integer or string, wakeupValue may not need to do anything at all. 
  4. The "awake" value gets returned to Page::getFieldValue(). With this value hand, Page::getFieldValue() remembers the value so it doesn't have to pull from DB again.
  5. If $page->outputFormatting is TRUE, Page::getFieldValue() runs the value through Fieldtype::formatValue(). That modifies the value for runtime output (example: applying an entity encoder to a text value). In the case of text fields, this would be when the value is routed through Textformatter modules. Note that formatValue() is called on every access to $page->fieldName, as PW only keeps the unformatted value in memory.
  6. Page::getFieldValue() returns the value, which you see as the result of your $page->fieldName call. 
Note that when you get a $page object, the fields aren't actually loaded until you try to access them. Meaning the above process occurs upon field access, not at page load. However, if the field has the "autojoin" option checked, then the above process is very different. However, those differences don't matter to any Fieldtype code (or any other code I can think of). 
 
As for error handing, this would typically be done in the Inputfield module, most commonly at Inputfield::processInput or in some cases Inputfield::setAttribute('value', 'my value'). However, it is also okay to trigger errors from Fieldtype::sanitizeValue, but just note that that method will be called on values regardless of whether they came from the API, user input or from the DB. Meaning, time consuming validations are best performed at the input (Inputfield) stage. 
 
 
I am am working on a little module that converts markup such as =(123:link text) to the respective hyperlink. 123 in the example is the page ID.
 
This sounds a lot like a Textformatter module. If that's your need, then your job is simple and you don't have to worry about any hooks or anything other than making a Textformatter module with Textformatter::format($value) or the newer Textformatter::formatValue($page, $field, $value). 
 
 
$this->addHookBefore('FieldtypeTextarea::wakeupValue', $this, 'wakeupValue');

$this->addHookBefore('FieldtypeTextareaLanguage::wakeupValue', $this, 'wakeupValue');

 
More likely you just want FieldtypeTextarea::wakeupValue, as that should be called for both instances above (FieldtypeTextareaLanguage extends FieldtypeTextarea). Most likely you are getting double calls having the two above hooks.
  • Like 8
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...