XF 2.3 Modifying a custom entity

Anatoliy

Well-known member
So, following the Building with XenForo 2 videos I created a Notes add-on. Now I would like to modify it to my needs. A simple modification - to replace a 'Title' field with a 'Fishing date' field. As I understand I need to modify my custom entity and also a setup file. So regarding a custom entity - I just replace
Code:
'title' => ['type' => self::STR, 'maxLength' => 255, 'required' => true, 'censor' => true],
with
Code:
'fishing_date' => ['type' => self::UINT, 'default' => \XF::$time],
?

Here is the whole code just in case
PHP:
<?php

namespace AV\Notes\Entity;

use XF\Mvc\Entity\Entity;
use XF\MVC\Entity\Structure;

class Note extends Entity
{
    protected function _preSave()
    {
        if ($this->isUpdate()) {
            $this->edit_date = \XF::$time;
        }
    }

    protected function verifyTitle(&$value)
    {
        if (strlen($value) < 3) {
            $this->error('Please anter a proper title', 'title');
            return false;
        }

        $value = utf8_ucwords($value);

        return true;
    }

    public function getFirstWord()
    {
        return explode($this->content, ' ')[0];
    }

    public static function getStructure(Structure $structure): Structure
    {
        $structure->table = 'av_notes_note';
        $structure->shortName = 'AV\Notes:Note';
        $structure->contentType = 'av_notes_note';
        $structure->primaryKey = 'note_id';
        $structure->columns = [
            'note_id' => ['type' => self::UINT, 'autoIncrement' => true],
            'user_id' => ['type' => self::UINT, 'default' => \XF::visitor()->user_id],
            'title' => ['type' => self::STR, 'maxLength' => 255, 'required' => true, 'censor' => true],
            'content' => ['type' => self::STR, 'required' => true],
            'post_date' => ['type' => self::UINT, 'default' => \XF::$time],
            'edit_date' => ['type' => self::UINT, 'default' => 0],
        ];
        $structure->relations = [
            'User' => [
                'entity' => 'XF:User',
                'type' => self::TO_ONE,
                'conditions' => 'user_id',
                'primary' => true,
            ],
        ];
        $structure->defaultWith = ['User'];
        $structure->getters = [
            'firstfWord' => true
        ];
        $structure->behaviors = [];

        return $structure;
    }
}

And regarding a setup file. What should I do? Should I add installStep2 that alters a table deleting the 'title' column and add installStep3 that alters a table adding the 'fishing_date' column?

PHP:
<?php

namespace AV\Notes;

use XF\AddOn\AbstractSetup;
use XF\AddOn\StepRunnerInstallTrait;
use XF\AddOn\StepRunnerUninstallTrait;
use XF\AddOn\StepRunnerUpgradeTrait;

class Setup extends AbstractSetup
{
    use StepRunnerInstallTrait;
    use StepRunnerUpgradeTrait;
    use StepRunnerUninstallTrait;

    public function installStep1()
    {
        $this->schemaManager()->createTable('av_notes_note', function (\XF\Db\Schema\Create $table) {
            $table->addColumn('note_id', 'int')->autoIncrement();
            $table->addColumn('user_id', 'int')->setDefault(0);
            $table->addColumn('title', 'varchar', 255);
            $table->addColumn('content', 'text');
            $table->addColumn('post_date', 'int')->setDefault(0);
            $table->addColumn('edit_date', 'int')->setDefault(0);
        });
    }

    public function uninstallStep1()
    {
        $this->schemaManager()->dropTable('av_notes_note');
    }
}

Please advise.
 
I think entity sholud be string like below:

PHP:
'fishing_date' => ['type' => self::STR, 'default' => \XF::$time],

And if it's update for your installed addon you sholud add your upgrade on setup file as below:

PHP:
public function upgrade1000020()
{
    $this->schemaManager()->alterTable('av_notes_note', function (Alter $table) {
        $table->addColumn('fishing_date', 'varchar', 255)->nullable();
    });
}

The 1000020 is version_id for your upgrade it should be as same as addon.json file, also it will be better to replace title column in installStep1 with fishing_date for any one install your addon on first time.
 
Thanks! So if my version id now is 1000011, and i'm gonna make this changes in 1000012, should I put xxx11 or xxx12 in the 'upgrade' function ?
 
Exactlly and if your upgrade version is 1000012 the setup file should be like this

PHP:
public function upgrade1000012()
{
    $this->schemaManager()->alterTable('av_notes_note', function (Alter $table) {
        $table->addColumn('fishing_date', 'varchar', 255)->nullable();
    });
}
 
The date should actually be a UINT to correspond with how XF treats/handles dates (pretty much everything uses UINT for a date column).
 
I guess I didn't understand something. I run
Code:
php cmd.php xf-addon:upgrade AV/Notes
and get
Code:
This add-on cannot be upgraded.


Code:
{
    "legacy_addon_id": "",
    "title": "Fishing notes",
    "description": "",
    "version_id": 1000011,
    "version_string": "1.0.0 Alpha 1",
    "dev": "",
    "dev_url": "",
    "faq_url": "",
    "support_url": "",
    "extra_urls": [],
    "require": [],
    "icon": "fa-comment-alt-edit"
}

Code:
<?php

namespace AV\Notes;

use XF\AddOn\AbstractSetup;
use XF\AddOn\StepRunnerInstallTrait;
use XF\AddOn\StepRunnerUninstallTrait;
use XF\AddOn\StepRunnerUpgradeTrait;

use XF\Db\Schema\Alter;
use XF\Db\Schema\Create;

class Setup extends AbstractSetup
{
    use StepRunnerInstallTrait;
    use StepRunnerUpgradeTrait;
    use StepRunnerUninstallTrait;

    public function installStep1()
    {
        $this->schemaManager()->createTable('av_notes_note', function (\XF\Db\Schema\Create $table) {
            $table->addColumn('note_id', 'int')->autoIncrement();
            $table->addColumn('user_id', 'int')->setDefault(0);
            $table->addColumn('fishing_date', 'int')->setDefault(0);
            $table->addColumn('content', 'text');
            $table->addColumn('post_date', 'int')->setDefault(0);
            $table->addColumn('edit_date', 'int')->setDefault(0);
        });
    }

    public function upgrade1000011()
    {
        $this->schemaManager()->alterTable('av_notes_note', function (Alter $table) {
            $table->addColumn('fishing_date', 'int')->setDefault(0);
        });
    }

    public function uninstallStep1()
    {
        $this->schemaManager()->dropTable('av_notes_note');
    }
}
 
I guess I didn't understand something. I run
Code:
php cmd.php xf-addon:upgrade AV/Notes
and get
Code:
This add-on cannot be upgraded.


Code:
{
    "legacy_addon_id": "",
    "title": "Fishing notes",
    "description": "",
    "version_id": 1000011,
    "version_string": "1.0.0 Alpha 1",
    "dev": "",
    "dev_url": "",
    "faq_url": "",
    "support_url": "",
    "extra_urls": [],
    "require": [],
    "icon": "fa-comment-alt-edit"
}

Code:
<?php

namespace AV\Notes;

use XF\AddOn\AbstractSetup;
use XF\AddOn\StepRunnerInstallTrait;
use XF\AddOn\StepRunnerUninstallTrait;
use XF\AddOn\StepRunnerUpgradeTrait;

use XF\Db\Schema\Alter;
use XF\Db\Schema\Create;

class Setup extends AbstractSetup
{
    use StepRunnerInstallTrait;
    use StepRunnerUpgradeTrait;
    use StepRunnerUninstallTrait;

    public function installStep1()
    {
        $this->schemaManager()->createTable('av_notes_note', function (\XF\Db\Schema\Create $table) {
            $table->addColumn('note_id', 'int')->autoIncrement();
            $table->addColumn('user_id', 'int')->setDefault(0);
            $table->addColumn('fishing_date', 'int')->setDefault(0);
            $table->addColumn('content', 'text');
            $table->addColumn('post_date', 'int')->setDefault(0);
            $table->addColumn('edit_date', 'int')->setDefault(0);
        });
    }

    public function upgrade1000011()
    {
        $this->schemaManager()->alterTable('av_notes_note', function (Alter $table) {
            $table->addColumn('fishing_date', 'int')->setDefault(0);
        });
    }

    public function uninstallStep1()
    {
        $this->schemaManager()->dropTable('av_notes_note');
    }
}
Your currently installed version is 1.0.0 Alpha 1. Since all you've done is add upgrade steps for a version that's already installed you won't be able to do anything with that command you posted.

To be honest if you're just working through the tutorial to make it your own and learn right now the best bet is to delete the table from Step 1 from your db, and re run the install cmd for step 1. Also since your first install step has already been changed to fishing_date you do not need the upgrade function you currently have.
 
Done! Thanks!!!
However I'm stuck right there )))

I'm trying to edit a form to replace
Code:
<xf:textboxrow name="title" value="{$note.title}" label="Title" />
with
Code:
<xf:date name="fishing_date" value="{$note.fishing_date}" />
and getting "
Template Compilation Error
public:av_notes_edit - Line 13: Tag date must have an attribute time. - Template name: public:av_notes_edit
"

PHP:
<xf:title>
    <xf:if is="$note.note_id">
        Edit note {$note.title}
        <xf:else />
        Add new note
    </xf:if>
</xf:title>

<xf:form action="{{ link('notes/save', $note) }}" class="block" ajax="1">
    <div class="block-container">
        <div class="block-body">
            <!-- <xf:textboxrow name="title" value="{$note.title}" label="Title" /> -->
            <xf:date name="fishing_date" value="{$note.fishing_date}" />
            <xf:textarearow name="content" value="{$note.content}" label="Your note" autosize="true" rows="5" />
        </div>
        <xf:submitrow submit="{{ phrase('save') }}" fa="fa-save" />
    </div>
</xf:form>
 
damn it! )))
it should be time="{$note.fishing_date}", not value="{$note.fishing_date}".
Thank you guys for helping!
 
I'm stuck with the fishing_date field and can't figure out what's wrong.

When I add a new note it puts '0' in fishing_date column, not a timestamp as in post_date column.
When I edit a note, selecting yesterdays date for fishing_date, it puts '2024' in fishing_date column, not a timestamp.

Please help, my head doesn't understand anything anymore. )

Code:
<xf:title>
    <xf:if is="$note.note_id">
        Edit note
        <xf:else />
        Add new note
    </xf:if>
</xf:title>

<xf:form action="{{ link('notes/save', $note) }}" class="block" ajax="1">
    <div class="block-container">
        <div class="block-body">
            <!-- <xf:textboxrow name="title" value="{$note.title}" label="Title" /> -->
            <!-- <xf:date name="fishing_date" time="{$note.fishing_date}" label="Fishing trip date" /> -->

            <xf:if is="$note.note_id">
                <xf:dateinputrow name="fishing_date" value="{$note.fishing_date}" label="Fishing trip date" />
            </xf:if>


            <xf:textarearow name="content" value="{$note.content}" label="Your note" autosize="true" rows="5" />
        </div>
        <xf:submitrow submit="{{ phrase('save') }}" fa="fa-save" />
    </div>
</xf:form>

PHP:
<?php

namespace AV\Notes\Pub\Controller;

use XF\Pub\Controller\AbstractController;
use XF\Mvc\ParameterBag;

class Note extends AbstractController
{
    protected function preDispatchController($action, ParameterBag $params)
    {
        $this->assertRegistrationRequired();
    }

    public function actionIndex(ParameterBag $params)
    {
        $noteFinder = $this->finder('AV\Notes:Note')
            ->where('user_id', \XF::visitor()->user_id)
            ->order('fishing_date', 'desc');

        $page = $params->page;
        $page = $this->filterPage();
        $perPage = 10;
        $noteFinder->limitByPage($page, $perPage);

        $viewParams = [
            'notes' => $noteFinder->fetch(),
            'page' => $page,
            'perPage' => $perPage,
            'total' => $noteFinder->total()
        ];

        return $this->view('AV\Notes:Note\Index', 'av_notes_index', $viewParams);
    }

    public function actionAdd()
    {
        $note = $this->em()->create('AV\Notes:Note');
        return $this->noteAddEdit($note);
    }

    public function actionEdit(ParameterBag $params)
    {
        $note = $this->assertNoteExists($params->note_id);
        return $this->noteAddEdit($note);
    }

    protected function noteAddEdit(\AV\Notes\Entity\Note $note)
    {
        $viewParams = [
            'note' => $note
        ];

        return $this->view('AV\Notes:Note\Edit', 'av_notes_edit', $viewParams);
    }

    public function actionSave(ParameterBag $params)
    {
        if ($params->note_id) {
            $note = $this->assertNoteExists($params->note_id);
        } else {
            $note = $this->em()->create('AV\Notes:Note');
        }

        $this->noteSaveProcess($note)->run();

        return $this->redirect($this->buildLink('notes'));
    }

    public function actionDelete(ParameterBag $params)
    {
        $note = $this->assertNoteExists($params->note_id);

        /** @var \XF\ControllerPlugin\Delete $plugin */
        $plugin = $this->plugin('XF:Delete');
        return $plugin->actionDelete(
            $note,
            $this->buildLink('notes/delete', $note),
            $this->buildLink('notes/edit', $note),
            $this->buildLink('notes'),
            $note->title
        );
    }

    protected function noteSaveProcess(\AV\Notes\Entity\Note $note)
    {
        $input = $this->filter([
            'fishing_date' => 'int',
            'content' => 'str'
        ]);

        $form = $this->formAction();
        $form->basicEntitySave($note, $input);
        return $form;
    }

    /**
     * @param $id
     * @param null $with
     * @param null $phraseKey
     *
     * @return \AV\Notes\Entity\Note
     * @throws \XF\Mvc\Reply\Exception
     */

    protected function assertNoteExists($id, $with = null, $phraseKey = null)
    {
        return $this->assertRecordExists('AV\Notes:Note', $id, $with, $phraseKey);
    }
}

PHP:
<?php

namespace AV\Notes\Entity;

use XF\Mvc\Entity\Entity;
use XF\MVC\Entity\Structure;

class Note extends Entity
{
    protected function _preSave()
    {
        if ($this->isUpdate()) {
            $this->edit_date = \XF::$time;
        }
    }

    protected function verifyTitle(&$value)
    {
        if (strlen($value) < 3) {
            $this->error('Please anter a proper title', 'title');
            return false;
        }

        $value = utf8_ucwords($value);

        return true;
    }

    public function getFirstWord()
    {
        return explode($this->content, ' ')[0];
    }

    public static function getStructure(Structure $structure): Structure
    {
        $structure->table = 'av_notes_note';
        $structure->shortName = 'AV\Notes:Note';
        $structure->contentType = 'av_notes_note';
        $structure->primaryKey = 'note_id';
        $structure->columns = [
            'note_id' => ['type' => self::UINT, 'autoIncrement' => true],
            'user_id' => ['type' => self::UINT, 'default' => \XF::visitor()->user_id],
            // 'title' => ['type' => self::STR, 'maxLength' => 255, 'required' => true, 'censor' => true],
            'fishing_date' => ['type' => self::UINT, 'default' => \XF::$time],
            'content' => ['type' => self::STR, 'required' => true],
            'post_date' => ['type' => self::UINT, 'default' => \XF::$time],
            'edit_date' => ['type' => self::UINT, 'default' => 0],
        ];
        $structure->relations = [
            'User' => [
                'entity' => 'XF:User',
                'type' => self::TO_ONE,
                'conditions' => 'user_id',
                'primary' => true,
            ],
        ];
        $structure->defaultWith = ['User'];
        $structure->getters = [
            'firstfWord' => true
        ];
        $structure->behaviors = [];

        return $structure;
    }
}
 
I'm stuck with the fishing_date field and can't figure out what's wrong.

When I add a new note it puts '0' in fishing_date column, not a timestamp as in post_date column.
When I edit a note, selecting yesterdays date for fishing_date, it puts '2024' in fishing_date column, not a timestamp.

Please help, my head doesn't understand anything anymore. )

Code:
<xf:title>
    <xf:if is="$note.note_id">
        Edit note
        <xf:else />
        Add new note
    </xf:if>
</xf:title>

<xf:form action="{{ link('notes/save', $note) }}" class="block" ajax="1">
    <div class="block-container">
        <div class="block-body">
            <!-- <xf:textboxrow name="title" value="{$note.title}" label="Title" /> -->
            <!-- <xf:date name="fishing_date" time="{$note.fishing_date}" label="Fishing trip date" /> -->

            <xf:if is="$note.note_id">
                <xf:dateinputrow name="fishing_date" value="{$note.fishing_date}" label="Fishing trip date" />
            </xf:if>


            <xf:textarearow name="content" value="{$note.content}" label="Your note" autosize="true" rows="5" />
        </div>
        <xf:submitrow submit="{{ phrase('save') }}" fa="fa-save" />
    </div>
</xf:form>

PHP:
<?php

namespace AV\Notes\Pub\Controller;

use XF\Pub\Controller\AbstractController;
use XF\Mvc\ParameterBag;

class Note extends AbstractController
{
    protected function preDispatchController($action, ParameterBag $params)
    {
        $this->assertRegistrationRequired();
    }

    public function actionIndex(ParameterBag $params)
    {
        $noteFinder = $this->finder('AV\Notes:Note')
            ->where('user_id', \XF::visitor()->user_id)
            ->order('fishing_date', 'desc');

        $page = $params->page;
        $page = $this->filterPage();
        $perPage = 10;
        $noteFinder->limitByPage($page, $perPage);

        $viewParams = [
            'notes' => $noteFinder->fetch(),
            'page' => $page,
            'perPage' => $perPage,
            'total' => $noteFinder->total()
        ];

        return $this->view('AV\Notes:Note\Index', 'av_notes_index', $viewParams);
    }

    public function actionAdd()
    {
        $note = $this->em()->create('AV\Notes:Note');
        return $this->noteAddEdit($note);
    }

    public function actionEdit(ParameterBag $params)
    {
        $note = $this->assertNoteExists($params->note_id);
        return $this->noteAddEdit($note);
    }

    protected function noteAddEdit(\AV\Notes\Entity\Note $note)
    {
        $viewParams = [
            'note' => $note
        ];

        return $this->view('AV\Notes:Note\Edit', 'av_notes_edit', $viewParams);
    }

    public function actionSave(ParameterBag $params)
    {
        if ($params->note_id) {
            $note = $this->assertNoteExists($params->note_id);
        } else {
            $note = $this->em()->create('AV\Notes:Note');
        }

        $this->noteSaveProcess($note)->run();

        return $this->redirect($this->buildLink('notes'));
    }

    public function actionDelete(ParameterBag $params)
    {
        $note = $this->assertNoteExists($params->note_id);

        /** @var \XF\ControllerPlugin\Delete $plugin */
        $plugin = $this->plugin('XF:Delete');
        return $plugin->actionDelete(
            $note,
            $this->buildLink('notes/delete', $note),
            $this->buildLink('notes/edit', $note),
            $this->buildLink('notes'),
            $note->title
        );
    }

    protected function noteSaveProcess(\AV\Notes\Entity\Note $note)
    {
        $input = $this->filter([
            'fishing_date' => 'int',
            'content' => 'str'
        ]);

        $form = $this->formAction();
        $form->basicEntitySave($note, $input);
        return $form;
    }

    /**
     * @param $id
     * @param null $with
     * @param null $phraseKey
     *
     * @return \AV\Notes\Entity\Note
     * @throws \XF\Mvc\Reply\Exception
     */

    protected function assertNoteExists($id, $with = null, $phraseKey = null)
    {
        return $this->assertRecordExists('AV\Notes:Note', $id, $with, $phraseKey);
    }
}

PHP:
<?php

namespace AV\Notes\Entity;

use XF\Mvc\Entity\Entity;
use XF\MVC\Entity\Structure;

class Note extends Entity
{
    protected function _preSave()
    {
        if ($this->isUpdate()) {
            $this->edit_date = \XF::$time;
        }
    }

    protected function verifyTitle(&$value)
    {
        if (strlen($value) < 3) {
            $this->error('Please anter a proper title', 'title');
            return false;
        }

        $value = utf8_ucwords($value);

        return true;
    }

    public function getFirstWord()
    {
        return explode($this->content, ' ')[0];
    }

    public static function getStructure(Structure $structure): Structure
    {
        $structure->table = 'av_notes_note';
        $structure->shortName = 'AV\Notes:Note';
        $structure->contentType = 'av_notes_note';
        $structure->primaryKey = 'note_id';
        $structure->columns = [
            'note_id' => ['type' => self::UINT, 'autoIncrement' => true],
            'user_id' => ['type' => self::UINT, 'default' => \XF::visitor()->user_id],
            // 'title' => ['type' => self::STR, 'maxLength' => 255, 'required' => true, 'censor' => true],
            'fishing_date' => ['type' => self::UINT, 'default' => \XF::$time],
            'content' => ['type' => self::STR, 'required' => true],
            'post_date' => ['type' => self::UINT, 'default' => \XF::$time],
            'edit_date' => ['type' => self::UINT, 'default' => 0],
        ];
        $structure->relations = [
            'User' => [
                'entity' => 'XF:User',
                'type' => self::TO_ONE,
                'conditions' => 'user_id',
                'primary' => true,
            ],
        ];
        $structure->defaultWith = ['User'];
        $structure->getters = [
            'firstfWord' => true
        ];
        $structure->behaviors = [];

        return $structure;
    }
}
It's most likely due to you pulling in fishing_date in the filter in noteSaveProcess which is probably turning it into a 0 before saving.
 
Try to remove fishing_date from filter in noteSaveProcess function:

PHP:
protected function noteSaveProcess(\AV\Notes\Entity\Note $note)
    {
        $input = $this->filter([
            'content' => 'str'
        ]);
    
        $input['fishing_date'] = \XF::$time;
        $form = $this->formAction();
        $form->basicEntitySave($note, $input);
        return $form;
    }
 
Try to remove fishing_date from filter in noteSaveProcess function:
Yeah, I already did that. Thanks! It adds timestamp now. My problem now is that I would like fishing_date to be editable as content is. Say if I add a note about my fishing trip that was not today but some time earlier. Can I acheave that without fishing_date in filter?
 
I thinks if i'm not wrong you need to use datepicker try this code:

HTML:
<xf:dateinput name="fishing_date" value="{$note.fishing_date}"  />
 
I thinks if i'm not wrong you need to use datepicker try this code:

HTML:
<xf:dateinput name="fishing_date" value="{$note.fishing_date}"  />
Yes I do.

Code:
<xf:title>
    <xf:if is="$note.note_id">
        Edit note
        <xf:else />
        Add new note
    </xf:if>
</xf:title>

<xf:form action="{{ link('notes/save', $note) }}" class="block" ajax="1">
    <div class="block-container">
        <div class="block-body">
            <xf:dateinputrow name="post_date" value="{$note.post_date}" label="Fishing trip date" />
            <xf:textarearow name="content" value="{$note.content}" label="Your note" autosize="true" rows="5" />
        </div>
        <xf:submitrow submit="{{ phrase('save') }}" fa="fa-save" />
    </div>
</xf:form>

I'm stuck not with an add/edit form template, but with saving data from the form to a db table.
In a tutorial there are 2 editable fields - title and content. They are placed in $input (filter). If I replace in filter title with post_date, not a unixtime is being saved to a table , but a year (2024). I can't figure out how the data from fields are being passed to controller and saved to db. I was thinking that with actionSave that calls to noteSaveProcess. But it looks like I didn't understand something.
.
 
I'm not sure but try to change fishing_date filter type in noteSaveProcess to datetime:

PHP:
protected function noteSaveProcess(\AV\Notes\Entity\Note $note)
    {
        $input = $this->filter([
            'content' => 'str',
            'fishing_date' => 'datetime'
        ]);
    
        $form = $this->formAction();
        $form->basicEntitySave($note, $input);
        return $form;
    }
 
Back
Top Bottom