From 71006450ab3dc67e45cb67e651c4511c29ef8650 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:36:05 +1100 Subject: [PATCH 01/22] (chore) bump change --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d2b7a4b..bffe539 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,5 @@ Check out [documentation](https://gorriecoe.github.io/silverstripe-link/en) - [Gorrie Coe](https://github.com/gorriecoe) - [Elliot Sawyer](https://github.com/elliot-sawyer) + + From eb2e130d2a2cafd0ff7cff7fc6b78e5e9a35501c Mon Sep 17 00:00:00 2001 From: JamesDPC <69664712+JamesDPC@users.noreply.github.com> Date: Mon, 27 Oct 2025 05:37:19 +0000 Subject: [PATCH 02/22] [rector] Automated updates generated by rector configuration --- composer.json | 4 +- src/extensions/AutomaticMarkupID.php | 3 +- src/extensions/DBStringLink.php | 5 +- src/extensions/DefineableMarkupID.php | 17 ++- src/extensions/LinkSiteTree.php | 32 +++-- src/extensions/SiteTreeLink.php | 3 +- src/models/Link.php | 180 +++++++++++++------------- src/view/Phone.php | 1 + 8 files changed, 125 insertions(+), 120 deletions(-) diff --git a/composer.json b/composer.json index 54a7251..0b5ec05 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,9 @@ "config": { "allow-plugins": { "composer/installers": true, - "silverstripe/vendor-plugin": true + "silverstripe/vendor-plugin": true, + "silverstripe/recipe-plugin": true, + "phpstan/extension-installer": true } }, "scripts": { diff --git a/src/extensions/AutomaticMarkupID.php b/src/extensions/AutomaticMarkupID.php index 571f6a1..bef05c0 100644 --- a/src/extensions/AutomaticMarkupID.php +++ b/src/extensions/AutomaticMarkupID.php @@ -9,6 +9,7 @@ * Add sitetree type to link field * * @package silverstripe-link + * @extends \SilverStripe\Core\Extension */ class AutomaticMarkupID extends Extension { @@ -17,7 +18,7 @@ class AutomaticMarkupID extends Extension */ public function updateIDValue(&$id) { - $owner = $this->owner; + $owner = $this->getOwner(); if ($owner->Title) { $id = Convert::raw2url($owner->Title); } diff --git a/src/extensions/DBStringLink.php b/src/extensions/DBStringLink.php index 28d9c6b..cfc89fa 100644 --- a/src/extensions/DBStringLink.php +++ b/src/extensions/DBStringLink.php @@ -10,6 +10,7 @@ * Adds methods to DBString to help manipulate the output suitable for links * * @package silverstripe-link + * @extends \SilverStripe\Core\Extension<(\SilverStripe\ORM\FieldType\DBString & static)> */ class DBStringLink extends Extension { @@ -18,7 +19,7 @@ class DBStringLink extends Extension */ public function LinkFriendly(): string { - return Convert::raw2url($this->owner->value); + return Convert::raw2url($this->getOwner()->value); } /** @@ -34,7 +35,7 @@ public function URLFriendly(): string */ public function PhoneFriendly(): string { - $value = $this->owner->value; + $value = $this->getOwner()->value; if ($value) { return Phone::create($value); } else { diff --git a/src/extensions/DefineableMarkupID.php b/src/extensions/DefineableMarkupID.php index e02b750..76b9a26 100644 --- a/src/extensions/DefineableMarkupID.php +++ b/src/extensions/DefineableMarkupID.php @@ -11,31 +11,30 @@ * Add sitetree type to link field * * @package silverstripe-link + * @property ?string $IDCustomValue + * @extends \SilverStripe\Core\Extension */ class DefineableMarkupID extends Extension { /** * Database fields - * @var array */ - private static $db = [ + private static array $db = [ 'IDCustomValue' => 'Text' ]; /** * Update Fields - * @return FieldList */ - public function updateCMSFields(FieldList $fields) + public function updateCMSFields(FieldList $fields): FieldList { - $owner = $this->owner; $fields->addFieldToTab( 'Root.Main', TextField::create( 'IDCustomValue', - _t(__CLASS__ . '.ID', 'ID') + _t(self::class . '.ID', 'ID') ) - ->setDescription(_t(__CLASS__ . '.IDCUSTOMVALUE', 'Define an ID for the link. This is particularly useful for google tracking.')) + ->setDescription(_t(self::class . '.IDCUSTOMVALUE', 'Define an ID for the link. This is particularly useful for google tracking.')) ); return $fields; } @@ -45,7 +44,7 @@ public function updateCMSFields(FieldList $fields) */ public function onBeforeWrite() { - $owner = $this->owner; + $owner = $this->getOwner(); $owner->IDCustomValue = Convert::raw2url($owner->IDCustomValue); } @@ -54,7 +53,7 @@ public function onBeforeWrite() */ public function updateIDValue(&$id) { - $owner = $this->owner; + $owner = $this->getOwner(); if ($owner->IDCustomValue) { $id = $owner->IDCustomValue; } diff --git a/src/extensions/LinkSiteTree.php b/src/extensions/LinkSiteTree.php index 03add41..a00e273 100644 --- a/src/extensions/LinkSiteTree.php +++ b/src/extensions/LinkSiteTree.php @@ -20,22 +20,23 @@ * @package silverstripe-link * * @property int $SiteTreeID + * @property ?string $Anchor + * @method mixed SiteTree() + * @extends \SilverStripe\Core\Extension */ class LinkSiteTree extends Extension { /** * Database fields - * @var array */ - private static $db = [ + private static array $db = [ 'Anchor' => 'Varchar(255)', ]; /** * Has_one relationship - * @var array */ - private static $has_one = [ + private static array $has_one = [ // @phpstan-ignore class.notFound 'SiteTree' => SiteTree::class, ]; @@ -43,10 +44,8 @@ class LinkSiteTree extends Extension /** * A map of object types that can be linked to * Custom dataobjects can be added to this - * - * @var array **/ - private static $types = [ + private static array $types = [ 'SiteTree' => 'Page on this website', ]; @@ -54,16 +53,15 @@ class LinkSiteTree extends Extension * Defines the label used in the sitetree dropdown. * @param String $sitetree_field_label */ - private static $sitetree_field_label = 'MenuTitle'; + private static string $sitetree_field_label = 'MenuTitle'; /** * Update Fields - * @param FieldList $fields */ public function updateCMSFields(FieldList $fields) { if(class_exists(SiteTree::class)) { - $owner = $this->owner; + $owner = $this->getOwner(); $config = $owner->config(); $sitetree_field_label = $config->get('sitetree_field_label') ? : 'MenuTitle'; @@ -73,29 +71,29 @@ public function updateCMSFields(FieldList $fields) Wrapper::create( $sitetreeField = TreeDropdownField::create( 'SiteTreeID', - _t(__CLASS__ . '.PAGE', 'Page'), + _t(self::class . '.PAGE', 'Page'), SiteTree::class ) ->setTitleField($sitetree_field_label), TextField::create( 'Anchor', - _t(__CLASS__ . '.ANCHOR', 'Anchor/Querystring') + _t(self::class . '.ANCHOR', 'Anchor/Querystring') ) - ->setDescription(_t(__CLASS__ . '.ANCHORINFO', 'Include # at the start of your anchor name or, ? at the start of your querystring')) + ->setDescription(_t(self::class . '.ANCHORINFO', 'Include # at the start of your anchor name or, ? at the start of your querystring')) ) ->displayIf('Type')->isEqualTo('SiteTree')->end() ); // Display warning if the selected page is deleted or unpublished if ($owner->SiteTreeID && !$owner->SiteTree()->isPublished()) { - $sitetreeField->setDescription(_t(__CLASS__ . '.DELETEDWARNING', 'Warning: The selected page appears to have been deleted or unpublished. This link may not appear or may be broken in the frontend')); + $sitetreeField->setDescription(_t(self::class . '.DELETEDWARNING', 'Warning: The selected page appears to have been deleted or unpublished. This link may not appear or may be broken in the frontend')); } } } public function updateIsCurrent(&$status): void { - $owner = $this->owner; + $owner = $this->getOwner(); if ( class_exists(SiteTree::class) && $owner->Type == 'SiteTree' && @@ -109,7 +107,7 @@ class_exists(SiteTree::class) && public function updateIsSection(&$status): void { - $owner = $this->owner; + $owner = $this->getOwner(); if ( class_exists(SiteTree::class) && $owner->Type == 'SiteTree' && @@ -123,7 +121,7 @@ class_exists(SiteTree::class) && public function updateIsOrphaned(&$status): void { - $owner = $this->owner; + $owner = $this->getOwner(); if ( class_exists(SiteTree::class) && $owner->Type == 'SiteTree' && diff --git a/src/extensions/SiteTreeLink.php b/src/extensions/SiteTreeLink.php index 0ca9443..44b412a 100644 --- a/src/extensions/SiteTreeLink.php +++ b/src/extensions/SiteTreeLink.php @@ -9,6 +9,7 @@ * Fixes duplicate link in SiteTree * * @package silverstripe-link + * @extends \SilverStripe\Core\Extension */ class SiteTreeLink extends Extension { @@ -17,7 +18,7 @@ class SiteTreeLink extends Extension */ public function onBeforeDuplicate() { - $owner = $this->owner; + $owner = $this->getOwner(); //loop through has_one relationships and reset any Link fields if($hasOne = $owner->Config()->get('has_one')){ foreach ($hasOne as $field => $fieldType) { diff --git a/src/models/Link.php b/src/models/Link.php index a8541cc..0cfa929 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -29,12 +29,12 @@ * @package silverstripe-link * * @property string $Title - * @property string $Type - * @property string $URL - * @property string $Email - * @property string $Phone + * @property ?string $Type + * @property ?string $URL + * @property ?string $Email + * @property ?string $Phone * @property bool $OpenInNewWindow - * @property string $SelectedStyle + * @property ?string $SelectedStyle * @property int $FileID * @method File File() * @mixin LinkSiteTree @@ -43,15 +43,13 @@ class Link extends DataObject { /** * Defines the database table name - * @var string */ - private static $table_name = 'Link'; + private static string $table_name = 'Link'; /** * Database fields - * @var array */ - private static $db = [ + private static array $db = [ 'Title' => 'Varchar', 'Type' => 'Varchar(50)', 'URL' => 'Text', @@ -63,22 +61,20 @@ class Link extends DataObject /** * Has_one relationship - * @var array */ - private static $has_one = [ + private static array $has_one = [ 'File' => File::class ]; - private static $owns = [ + private static array $owns = [ 'File', ]; /** * Defines summary fields commonly used in table columns * as a quick overview of the data for this dataobject - * @var array */ - private static $summary_fields = [ + private static array $summary_fields = [ 'Title' => 'Title', 'TypeLabel' => 'Type', 'LinkURL' => 'Link' @@ -86,9 +82,8 @@ class Link extends DataObject /** * Defines a default list of filters for the search context - * @var array */ - private static $searchable_fields = [ + private static array $searchable_fields = [ 'Title', 'URL', 'Email', @@ -98,18 +93,14 @@ class Link extends DataObject /** * A map of styles that are available in the cms for * users to select from. - * - * @var array */ - private static $styles = []; + private static array $styles = []; /** * A map of object types that can be linked to * Custom dataobjects can be added to this - * - * @var array */ - private static $types = [ + private static array $types = [ 'URL' => 'URL', 'Email' => 'Email address', 'Phone' => 'Phone number', @@ -121,14 +112,13 @@ class Link extends DataObject * * @var array */ - private static $allowed_types = null; + private static $allowed_types; /** * Ensures that the methods are wrapped in the correct type and * values are safely escaped while rendering in the template. - * @var array */ - private static $casting = [ + private static array $casting = [ 'ClassAttr' => 'HTMLFragment', 'TargetAttr' => 'HTMLFragment', 'IDAttr' => 'HTMLFragment' @@ -136,40 +126,35 @@ class Link extends DataObject /** * @config - * @var string */ - private static $linking_mode_default = 'link'; + private static string $linking_mode_default = 'link'; /** * @config - * @var string */ - private static $linking_mode_current = 'current'; + private static string $linking_mode_current = 'current'; /** * @config - * @var string */ - private static $linking_mode_section = 'section'; + private static string $linking_mode_section = 'section'; /** * If false, when Type is "File", folders in the TreeDropdownField will not be selectable. * @config - * @var boolean */ - private static $link_to_folders = false; + private static bool $link_to_folders = false; /** * Provides a quick way to define additional methods for provideGraphQLScaffolding as Fields * @return Array */ - private static $gql_fields = []; + private static array $gql_fields = []; /** * Provides a quick way to define additional methods for provideGraphQLScaffolding as Nested Queries - * @var Array */ - private static $gql_nested_queries = []; + private static array $gql_nested_queries = []; /** * Custom CSS classes for template @@ -186,6 +171,7 @@ class Link extends DataObject * CMS Fields * @return FieldList */ + #[\Override] public function getCMSFields() { $fields = FieldList::create( @@ -206,10 +192,10 @@ public function getCMSFields() 'Root.Settings', DropdownField::create( 'SelectedStyle', - _t(__CLASS__ . '.STYLE', 'Style'), + _t(self::class . '.STYLE', 'Style'), $styles ) - ->setEmptyString(_t(__CLASS__ . '.DEFAULT', 'Default')), + ->setEmptyString(_t(self::class . '.DEFAULT', 'Default')), 'Type' ); } @@ -227,27 +213,25 @@ public function getCMSFields() /** * CMS Main fields * This is so other modules can access these fields without other tabs etc. - * - * @return Array */ - public function getCMSMainFields() + public function getCMSMainFields(): array { $fields = [ TextField::create( 'Title', - _t(__CLASS__ . '.TITLE', 'Title') + _t(self::class . '.TITLE', 'Title') ) - ->setDescription(_t(__CLASS__ . '.OPTIONALTITLE', 'Optional. Will be auto-generated from link if left blank.')), + ->setDescription(_t(self::class . '.OPTIONALTITLE', 'Optional. Will be auto-generated from link if left blank.')), OptionsetField::create( 'Type', - _t(__CLASS__ . '.LINKTYPE', 'Type'), + _t(self::class . '.LINKTYPE', 'Type'), $this->i18nTypes ) ->setValue('URL'), Wrapper::create( $fileDropdown = TreeDropdownField::create( 'FileID', - _t(__CLASS__ . '.FILE', 'File'), + _t(self::class . '.FILE', 'File'), File::class, 'ID', 'Title' @@ -257,27 +241,27 @@ public function getCMSMainFields() Wrapper::create( TextField::create( 'URL', - _t(__CLASS__ . '.URL', 'URL') + _t(self::class . '.URL', 'URL') ) ) ->displayIf('Type')->isEqualTo('URL')->end(), Wrapper::create( TextField::create( 'Email', - _t(__CLASS__ . '.EMAILADDRESS', 'Email Address') + _t(self::class . '.EMAILADDRESS', 'Email Address') ) ) ->displayIf('Type')->isEqualTo('Email')->end(), Wrapper::create( TextField::create( 'Phone', - _t(__CLASS__ . '.PHONENUMBER', 'Phone Number') + _t(self::class . '.PHONENUMBER', 'Phone Number') ) ) ->displayIf('Type')->isEqualTo('Phone')->end(), CheckboxField::create( 'OpenInNewWindow', - _t(__CLASS__ . '.OPENINNEWWINDOW','Open link in a new window') + _t(self::class . '.OPENINNEWWINDOW','Open link in a new window') ) ->displayIf('Type')->isEqualTo('URL') ->orIf()->isEqualTo('File') @@ -286,9 +270,7 @@ public function getCMSMainFields() // Disable folders in dropdown if linking to folders is not allowed. if (!$this->config()->get('link_to_folders')) { - $fileDropdown->setDisableFunction(function ($item) { - return is_a($item, Folder::class); - }); + $fileDropdown->setDisableFunction(fn($item): bool => is_a($item, Folder::class)); } $this->extend('updateCMSMainFields', $fields); @@ -299,6 +281,7 @@ public function getCMSMainFields() /** * Validate */ + #[\Override] public function validate(): ValidationResult { $valid = true; @@ -313,28 +296,31 @@ public function validate(): ValidationResult if ($this->{$type} == '') { $valid = false; $message = _t( - __CLASS__ . '.VALIDATIONERROR_EMPTY'.strtoupper($type), + self::class . '.VALIDATIONERROR_EMPTY'.strtoupper($type), 'You must enter a {TypeLabel}', [ 'TypeLabel' => $this->TypeLabel ] ); } + break; case 'File': case 'SiteTree': if (empty($this->{$type.'ID'})) { $valid = false; $message = _t( - __CLASS__ . '.VALIDATIONERROR_OBJECT', + self::class . '.VALIDATIONERROR_OBJECT', 'Please select a {TypeLabel}', [ 'TypeLabel' => $this->TypeLabel ] ); } + break; } + // if its already failed don't bother checking the rest if ($valid) { switch ($type) { @@ -343,28 +329,31 @@ public function validate(): ValidationResult if (!in_array(substr($this->URL, 0, 1), $allowedFirst) && !filter_var($this->URL, FILTER_VALIDATE_URL)) { $valid = false; $message = _t( - __CLASS__ . '.VALIDATIONERROR_VALIDURL', + self::class . '.VALIDATIONERROR_VALIDURL', 'Please enter a valid URL. Be sure to include http:// for an external URL. or begin your internal url/anchor with a "/" character' ); } + break; case 'Email': if (!filter_var($this->Email, FILTER_VALIDATE_EMAIL)) { $valid = false; $message = _t( - __CLASS__ . '.VALIDATIONERROR_VALIDEMAIL', + self::class . '.VALIDATIONERROR_VALIDEMAIL', 'Please enter a valid Email address' ); } + break; case 'Phone': if (!preg_match("/^\+?[0-9a-zA-Z\-\s]*[\,\#]?[0-9\-\s]*$/", $this->Phone)) { $valid = false; $message = _t( - __CLASS__ . '.VALIDATIONERROR_VALIDPHONE', + self::class . '.VALIDATIONERROR_VALIDPHONE', 'Please enter a valid Phone number' ); } + break; } } @@ -383,6 +372,7 @@ public function validate(): ValidationResult * Event handler called before writing to the database. * If the title is empty, set a default based on the link. */ + #[\Override] public function onBeforeWrite() { parent::onBeforeWrite(); @@ -401,6 +391,7 @@ public function onBeforeWrite() $this->Title = $siteTree->MenuTitle; } } + break; default: if ($this->getRelationType($type) == 'has_one' && $component = $this->getComponent($type)) { @@ -408,6 +399,7 @@ public function onBeforeWrite() } else { $this->Title = 'Link-' . $this->ID; } + break; } } @@ -415,14 +407,12 @@ public function onBeforeWrite() /** * Provides a quick way to define additional methods to provideGraphQLScaffolding as Fields - * @return Array */ - public function gqlFields() + public function gqlFields(): array { $fields = $this->config()->get('gql_fields'); $this->extend('updateGqlFields', $fields); - $fields = array_merge(['LinkURL'], $fields); - return $fields; + return array_merge(['LinkURL'], $fields); } /** @@ -439,14 +429,14 @@ public function gqlNestedQueries() /** * Set CSS classes for templates * @param string $class CSS classes. - * @return Link */ - public function addExtraClass($class) + public function addExtraClass($class): static { $classes = ($class) ? explode(' ', $class) : []; - foreach ($classes as $key => $value) { + foreach ($classes as $value) { $this->classes[$value] = $value; } + return $this; } @@ -463,9 +453,8 @@ public function setClass($class) /** * Set style used for * @param string $style - * @return Link */ - public function setStyle($style) + public function setStyle($style): static { $this->template_style = $style; return $this; @@ -476,16 +465,15 @@ public function setStyle($style) */ public function getStyle(): ?string { - return $this->SelectedStyle ? $this->SelectedStyle : $this->template_style; + return $this->SelectedStyle ?: $this->template_style; } /** * Sets allowed link types * * @param array $types Allowed type names - * @return Link */ - public function setAllowedTypes($types = []) + public function setAllowedTypes($types = []): static { $this->allowed_types = $types; return $this; @@ -504,6 +492,7 @@ public function getTypes() // Prioritise local field over global settings $allowed_types = $this->allowed_types; } + if ($allowed_types) { foreach ($allowed_types as $type) { if (!array_key_exists($type, $types)) { @@ -511,25 +500,26 @@ public function getTypes() } } - foreach (array_diff_key($types, array_flip($allowed_types)) as $key => $value) { + foreach (array_keys(array_diff_key($types, array_flip($allowed_types))) as $key) { unset($types[$key]); } } + $this->extend('updateTypes', $types); return $types; } /** * Returns allowed link types with translations - * @return array */ - public function geti18nTypes() + public function geti18nTypes(): array { $i18nTypes = []; // Get translatable labels foreach ($this->Types as $key => $label) { - $i18nTypes[$key] = _t(__CLASS__ . '.TYPE'.strtoupper($key), $label); + $i18nTypes[$key] = _t(self::class . '.TYPE'.strtoupper($key), $label); } + $this->extend('updatei18nTypes', $i18nTypes); return $i18nTypes; } @@ -547,14 +537,14 @@ public function getStyles() /** * Returns available styles with translations - * @return array */ - public function geti18nStyles() + public function geti18nStyles(): array { $i18nStyles = []; foreach ($this->styles as $key => $label) { - $i18nStyles[$key] = _t(__CLASS__ . '.STYLE' . strtoupper($key), $label); + $i18nStyles[$key] = _t(self::class . '.STYLE' . strtoupper($key), $label); } + $this->extend('updatei18nStyles', $i18nStyles); return $i18nStyles; } @@ -567,6 +557,7 @@ public function getLinkURL(): ?string if (!$this->ID) { return null; } + $type = $this->Type; switch ($type) { case 'URL': @@ -584,11 +575,12 @@ public function getLinkURL(): ?string if (!$component->exists()) { $LinkURL = null; } + if ($component->hasMethod('Link')) { $LinkURL = $component->Link() . $this->Anchor; } else { $LinkURL = _t( - __CLASS__ . '.LINKMETHODMISSING', + self::class . '.LINKMETHODMISSING', 'Please implement a Link() method on your dataobject "{type}"', [ 'type' => $type @@ -596,6 +588,7 @@ public function getLinkURL(): ?string ); } } + break; default: $LinkURL = null; @@ -613,15 +606,16 @@ public function getClass(): string { if ($this->SelectedStyle) { $this->setClass($this->SelectedStyle); - } else if ($this->template_style) { + } elseif ($this->template_style) { $this->setClass($this->template_style); } $classes = $this->classes; $this->extend('updateClasses', $classes); - if (count($classes)) { + if ($classes !== []) { return implode(' ', $classes); } + return ''; } @@ -641,7 +635,7 @@ public function getClassAttr(): string /** * Returns the html target attribute */ - public function getTarget() + public function getTarget(): string { return $this->OpenInNewWindow ? "_blank" : ''; } @@ -686,6 +680,7 @@ public function getCurrentPage() if (class_exists(SiteTree::class) && class_exists(ContentController::class) && ($currentPage instanceof ContentController)) { $currentPage = $currentPage->data(); } + return $currentPage; } @@ -786,15 +781,14 @@ public function LinkingMode() public function getTypeLabel() { $types = $this->config()->get('types'); - return isset($types[$this->Type]) ? _t(__CLASS__ . '.TYPE' . strtoupper($this->Type), $types[$this->Type]) : null; + return isset($types[$this->Type]) ? _t(self::class . '.TYPE' . strtoupper($this->Type), $types[$this->Type]) : null; } /** * Returns the base class without namespacing * @param string $class - * @return string */ - public function baseClassName($class) + public function baseClassName($class): string { $class = explode('\\', $class); return array_pop($class); @@ -803,12 +797,14 @@ public function baseClassName($class) /** * Renders an HTML anchor attribute for this link */ + #[\Override] public function forTemplate(): string { $link = ''; if ($this->LinkURL) { $link = $this->renderWith($this->RenderTemplates); } + $this->extend('updateTemplate', $link); return $link; } @@ -816,23 +812,22 @@ public function forTemplate(): string /** * Renders an HTML anchor tag for this link * This is an alias to {@link forTemplate()} - * - * @return string */ - public function getLayout() + public function getLayout(): string { return $this->forTemplate(); } /** * Returns a list of rendering templates - * @return array */ - public function getRenderTemplates() + public function getRenderTemplates(): array { $ClassName = $this->ClassName; - if (is_object($ClassName)) $ClassName = get_class($ClassName); + if (is_object($ClassName)) { + $ClassName = $ClassName::class; + } if (!is_subclass_of($ClassName, DataObject::class)) { throw new InvalidArgumentException($ClassName . ' is not a subclass of DataObject'); @@ -846,12 +841,15 @@ public function getRenderTemplates() if ($this->Style) { $templates[] = $baseClassName . '_' . $this->style; } + $templates[] = $baseClassName; if ($next == DataObject::class) { return $templates; } + $ClassName = $next; } + return []; } @@ -859,6 +857,7 @@ public function getRenderTemplates() * @param \SilverStripe\Security\Member|null $member * @return bool */ + #[\Override] public function canView($member = null) { return true; @@ -868,6 +867,7 @@ public function canView($member = null) * @param \SilverStripe\Security\Member|null $member * @return bool */ + #[\Override] public function canEdit($member = null) { return true; @@ -877,6 +877,7 @@ public function canEdit($member = null) * @param \SilverStripe\Security\Member|null $member * @return bool */ + #[\Override] public function canDelete($member = null) { return true; @@ -887,6 +888,7 @@ public function canDelete($member = null) * @param array $context * @return bool */ + #[\Override] public function canCreate($member = null, $context = []) { return true; diff --git a/src/view/Phone.php b/src/view/Phone.php index 3bbc236..bd2bc06 100644 --- a/src/view/Phone.php +++ b/src/view/Phone.php @@ -115,6 +115,7 @@ public function Render(): string } } + #[\Override] public function forTemplate(): string { return $this->Render(); From 89b9aa8168c26a9cfc7fc06f4a8e4bbf5feb01fa Mon Sep 17 00:00:00 2001 From: JamesDPC <69664712+JamesDPC@users.noreply.github.com> Date: Mon, 27 Oct 2025 05:37:20 +0000 Subject: [PATCH 03/22] [php-cs-fixer] Automated updates generated by php-cs-fixer configuration --- .php-cs-fixer.cache | 1 + src/extensions/LinkSiteTree.php | 6 +++--- src/extensions/SiteTreeLink.php | 2 +- src/models/Link.php | 19 +++++++++---------- src/view/Phone.php | 2 -- 5 files changed, 14 insertions(+), 16 deletions(-) create mode 100644 .php-cs-fixer.cache diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache new file mode 100644 index 0000000..d6b5566 --- /dev/null +++ b/.php-cs-fixer.cache @@ -0,0 +1 @@ +{"php":"8.3.26","version":"3.89.1:v3.89.1#f34967da2866ace090a2b447de1f357356474573","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_indentation":true,"array_syntax":{"syntax":"short"},"no_unused_imports":true},"hashes":{"src\/models\/Link.php":"605ede16f81a126f7fbb426a342939de","src\/extensions\/AutomaticMarkupID.php":"b88b4da92deb7ba0e905c82dfd5d6ecd","src\/extensions\/DBStringLink.php":"1de17c8e908fc52aad2029ecd7f1bf8b","src\/extensions\/DefineableMarkupID.php":"ac1b789b94556c2ec2eed0760ea7e5fa","src\/extensions\/LinkSiteTree.php":"e98c5455fde305ba422c64ddf05561e8","src\/extensions\/SiteTreeLink.php":"a9974e33067a240882c7214c9ae8b70c","src\/view\/Phone.php":"4f2bfddd0cd029021a00ef91b1311f05"}} \ No newline at end of file diff --git a/src/extensions/LinkSiteTree.php b/src/extensions/LinkSiteTree.php index a00e273..f8ebf09 100644 --- a/src/extensions/LinkSiteTree.php +++ b/src/extensions/LinkSiteTree.php @@ -10,7 +10,7 @@ use SilverStripe\Core\Extension; use UncleCheese\DisplayLogic\Forms\Wrapper; -if(!class_exists(SiteTree::class)) { +if (!class_exists(SiteTree::class)) { return; } @@ -60,10 +60,10 @@ class LinkSiteTree extends Extension */ public function updateCMSFields(FieldList $fields) { - if(class_exists(SiteTree::class)) { + if (class_exists(SiteTree::class)) { $owner = $this->getOwner(); $config = $owner->config(); - $sitetree_field_label = $config->get('sitetree_field_label') ? : 'MenuTitle'; + $sitetree_field_label = $config->get('sitetree_field_label') ?: 'MenuTitle'; // Insert site tree field after the file selection field $fields->insertAfter( diff --git a/src/extensions/SiteTreeLink.php b/src/extensions/SiteTreeLink.php index 44b412a..2841da9 100644 --- a/src/extensions/SiteTreeLink.php +++ b/src/extensions/SiteTreeLink.php @@ -20,7 +20,7 @@ public function onBeforeDuplicate() { $owner = $this->getOwner(); //loop through has_one relationships and reset any Link fields - if($hasOne = $owner->Config()->get('has_one')){ + if ($hasOne = $owner->Config()->get('has_one')) { foreach ($hasOne as $field => $fieldType) { if ($fieldType === Link::class) { $owner->{$field.'ID'} = 0; diff --git a/src/models/Link.php b/src/models/Link.php index 0cfa929..4f1c170 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -3,7 +3,6 @@ namespace gorriecoe\Link\Models; use gorriecoe\Link\Extensions\LinkSiteTree; -use gorriecoe\Link\Extensions\SiteTreeLink; use InvalidArgumentException; use SilverStripe\Assets\File; use SilverStripe\CMS\Model\SiteTree; @@ -65,9 +64,9 @@ class Link extends DataObject private static array $has_one = [ 'File' => File::class ]; - + private static array $owns = [ - 'File', + 'File', ]; /** @@ -261,7 +260,7 @@ public function getCMSMainFields(): array ->displayIf('Type')->isEqualTo('Phone')->end(), CheckboxField::create( 'OpenInNewWindow', - _t(self::class . '.OPENINNEWWINDOW','Open link in a new window') + _t(self::class . '.OPENINNEWWINDOW', 'Open link in a new window') ) ->displayIf('Type')->isEqualTo('URL') ->orIf()->isEqualTo('File') @@ -270,7 +269,7 @@ public function getCMSMainFields(): array // Disable folders in dropdown if linking to folders is not allowed. if (!$this->config()->get('link_to_folders')) { - $fileDropdown->setDisableFunction(fn($item): bool => is_a($item, Folder::class)); + $fileDropdown->setDisableFunction(fn ($item): bool => is_a($item, Folder::class)); } $this->extend('updateCMSMainFields', $fields); @@ -385,9 +384,9 @@ public function onBeforeWrite() $this->Title = $this->getField($type); break; case 'SiteTree': - if(class_exists(SiteTree::class) && $this->hasMethod('SiteTree')) { + if (class_exists(SiteTree::class) && $this->hasMethod('SiteTree')) { $siteTree = $this->SiteTree(); - if($siteTree instanceof SiteTree) { + if ($siteTree instanceof SiteTree) { $this->Title = $siteTree->MenuTitle; } } @@ -494,7 +493,7 @@ public function getTypes() } if ($allowed_types) { - foreach ($allowed_types as $type) { + foreach ($allowed_types as $type) { if (!array_key_exists($type, $types)) { user_error("{$type} is not a valid link type"); } @@ -625,7 +624,7 @@ public function getClass(): string public function getClassAttr(): string { $class = trim($this->getClass()); - if($class !== '') { + if ($class !== '') { return ' class="' . Convert::raw2htmlatt($class) . '"'; } else { return ''; @@ -664,7 +663,7 @@ public function getIDValue(): ?string public function getIDAttr(): string { $idValue = trim($this->getIDValue() ?? ''); - if($idValue !== '') { + if ($idValue !== '') { return ' id="' . $idValue . '"'; } else { return ''; diff --git a/src/view/Phone.php b/src/view/Phone.php index bd2bc06..b53429b 100644 --- a/src/view/Phone.php +++ b/src/view/Phone.php @@ -4,7 +4,6 @@ use libphonenumber\PhoneNumberFormat; use libphonenumber\PhoneNumberUtil; -use libphonenumber\PhoneNumber; use SilverStripe\Model\ModelData; /** @@ -12,7 +11,6 @@ */ class Phone extends ModelData { - protected \libphonenumber\PhoneNumberUtil $library; protected \libphonenumber\PhoneNumber $instance; From 5f8d8643825d035fcc5cf04cc4f2f5f2e04b1480 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:00:24 +1100 Subject: [PATCH 04/22] (dev) ignore cache file --- .gitignore | 1 + .php-cs-fixer.cache | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 .php-cs-fixer.cache diff --git a/.gitignore b/.gitignore index 771f21c..42263ff 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ vendor /composer.lock /vendor/ /public/ +/.php-cs-fixer.cache diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache deleted file mode 100644 index d6b5566..0000000 --- a/.php-cs-fixer.cache +++ /dev/null @@ -1 +0,0 @@ -{"php":"8.3.26","version":"3.89.1:v3.89.1#f34967da2866ace090a2b447de1f357356474573","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_indentation":true,"array_syntax":{"syntax":"short"},"no_unused_imports":true},"hashes":{"src\/models\/Link.php":"605ede16f81a126f7fbb426a342939de","src\/extensions\/AutomaticMarkupID.php":"b88b4da92deb7ba0e905c82dfd5d6ecd","src\/extensions\/DBStringLink.php":"1de17c8e908fc52aad2029ecd7f1bf8b","src\/extensions\/DefineableMarkupID.php":"ac1b789b94556c2ec2eed0760ea7e5fa","src\/extensions\/LinkSiteTree.php":"e98c5455fde305ba422c64ddf05561e8","src\/extensions\/SiteTreeLink.php":"a9974e33067a240882c7214c9ae8b70c","src\/view\/Phone.php":"4f2bfddd0cd029021a00ef91b1311f05"}} \ No newline at end of file From dac137dabd908780aa0aae4e4539f75cd9b9db2e Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:01:21 +1100 Subject: [PATCH 05/22] More updates based on analysis --- src/extensions/AutomaticMarkupID.php | 5 ++- src/extensions/DBStringLink.php | 2 +- src/extensions/DefineableMarkupID.php | 9 ++-- src/extensions/LinkSiteTree.php | 59 +++++++++++++++------------ src/extensions/SiteTreeLink.php | 4 +- 5 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/extensions/AutomaticMarkupID.php b/src/extensions/AutomaticMarkupID.php index bef05c0..b59cb26 100644 --- a/src/extensions/AutomaticMarkupID.php +++ b/src/extensions/AutomaticMarkupID.php @@ -2,6 +2,7 @@ namespace gorriecoe\Link\Extensions; +use gorriecoe\Link\Models\Link; use SilverStripe\Core\Convert; use SilverStripe\Core\Extension; @@ -19,8 +20,8 @@ class AutomaticMarkupID extends Extension public function updateIDValue(&$id) { $owner = $this->getOwner(); - if ($owner->Title) { - $id = Convert::raw2url($owner->Title); + if (($owner instanceof Link) && $owner->Title) { + $id = Convert::raw2url($owner->Title ?? ''); } } } diff --git a/src/extensions/DBStringLink.php b/src/extensions/DBStringLink.php index cfc89fa..c4e4844 100644 --- a/src/extensions/DBStringLink.php +++ b/src/extensions/DBStringLink.php @@ -19,7 +19,7 @@ class DBStringLink extends Extension */ public function LinkFriendly(): string { - return Convert::raw2url($this->getOwner()->value); + return Convert::raw2url($this->getOwner()->value ?? ''); } /** diff --git a/src/extensions/DefineableMarkupID.php b/src/extensions/DefineableMarkupID.php index 76b9a26..ca1ace7 100644 --- a/src/extensions/DefineableMarkupID.php +++ b/src/extensions/DefineableMarkupID.php @@ -2,6 +2,7 @@ namespace gorriecoe\Link\Extensions; +use gorriecoe\Link\Models\Link; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\TextField; use SilverStripe\Core\Extension; @@ -45,16 +46,18 @@ public function updateCMSFields(FieldList $fields): FieldList public function onBeforeWrite() { $owner = $this->getOwner(); - $owner->IDCustomValue = Convert::raw2url($owner->IDCustomValue); + if($owner instanceof Link) { + $owner->IDCustomValue = Convert::raw2url($owner->IDCustomValue ?? ''); + } } /** * Renders an HTML ID attribute for this link */ - public function updateIDValue(&$id) + public function updateIDValue(&$id): void { $owner = $this->getOwner(); - if ($owner->IDCustomValue) { + if (($owner instanceof Link) && $owner->IDCustomValue) { $id = $owner->IDCustomValue; } } diff --git a/src/extensions/LinkSiteTree.php b/src/extensions/LinkSiteTree.php index f8ebf09..57fc2c0 100644 --- a/src/extensions/LinkSiteTree.php +++ b/src/extensions/LinkSiteTree.php @@ -4,6 +4,7 @@ use gorriecoe\Link\Models\Link; use SilverStripe\CMS\Model\SiteTree; +use SilverStripe\Core\Config\Config; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\TreeDropdownField; use SilverStripe\Forms\TextField; @@ -51,7 +52,7 @@ class LinkSiteTree extends Extension /** * Defines the label used in the sitetree dropdown. - * @param String $sitetree_field_label + * @param string $sitetree_field_label */ private static string $sitetree_field_label = 'MenuTitle'; @@ -60,10 +61,10 @@ class LinkSiteTree extends Extension */ public function updateCMSFields(FieldList $fields) { - if (class_exists(SiteTree::class)) { - $owner = $this->getOwner(); - $config = $owner->config(); - $sitetree_field_label = $config->get('sitetree_field_label') ?: 'MenuTitle'; + $owner = $this->getOwner(); + if (class_exists(SiteTree::class) && ($owner instanceof Link)) { + + $sitetree_field_label = Config::inst()->get($owner::class, 'sitetree_field_label') ?: 'MenuTitle'; // Insert site tree field after the file selection field $fields->insertAfter( @@ -85,7 +86,8 @@ public function updateCMSFields(FieldList $fields) ); // Display warning if the selected page is deleted or unpublished - if ($owner->SiteTreeID && !$owner->SiteTree()->isPublished()) { + $siteTree = $owner->SiteTree(); + if ($siteTree->isInDB() && !$siteTree->isPublished()) { $sitetreeField->setDescription(_t(self::class . '.DELETEDWARNING', 'Warning: The selected page appears to have been deleted or unpublished. This link may not appear or may be broken in the frontend')); } } @@ -96,12 +98,13 @@ public function updateIsCurrent(&$status): void $owner = $this->getOwner(); if ( class_exists(SiteTree::class) && - $owner->Type == 'SiteTree' && - isset($owner->SiteTreeID) && - ($owner->CurrentPage instanceof SiteTree) + ($owner instanceof Link) && + $owner->Type == 'SiteTree' ) { - $currentPage = $owner->CurrentPage; - $status = $currentPage === $owner->SiteTree() || $currentPage->ID === $owner->SiteTreeID; + $currentPage = $owner->getCurrentPage(); + if($currentPage instanceof SiteTree) { + $status = $currentPage === $owner->SiteTree() || $currentPage->ID === $owner->SiteTreeID; + } } } @@ -110,12 +113,13 @@ public function updateIsSection(&$status): void $owner = $this->getOwner(); if ( class_exists(SiteTree::class) && - $owner->Type == 'SiteTree' && - isset($owner->SiteTreeID) && - ($owner->CurrentPage instanceof SiteTree) + ($owner instanceof Link) && + $owner->Type == 'SiteTree' ) { - $currentPage = $owner->CurrentPage; - $status = $owner->isCurrent() || in_array($owner->SiteTreeID, $currentPage->getAncestors()->column()); + $currentPage = $owner->getCurrentPage(); + if($currentPage instanceof SiteTree) { + $status = $owner->isCurrent() || in_array($owner->SiteTreeID, $currentPage->getAncestors()->column()); + } } } @@ -124,18 +128,19 @@ public function updateIsOrphaned(&$status): void $owner = $this->getOwner(); if ( class_exists(SiteTree::class) && - $owner->Type == 'SiteTree' && - isset($owner->SiteTreeID) && - ($owner->CurrentPage instanceof SiteTree) + ($owner instanceof Link) && + $owner->Type == 'SiteTree' ) { - $currentPage = $owner->CurrentPage; - // Always false for root pages - if (empty($owner->SiteTree()->ParentID)) { - $status = false; - } else { - // Parent must exist and not be an orphan itself - $parent = $owner->Parent(); - $status = !$parent || !$parent->exists() || $parent->isOrphaned(); + $currentPage = $owner->getCurrentPage(); + if($currentPage instanceof SiteTree) { + // Always false for root pages + if (empty($owner->SiteTree()->ParentID)) { + $status = false; + } else { + // Parent must exist and not be an orphan itself + $parent = $owner->Parent(); + $status = !$parent || !$parent->exists() || $parent->isOrphaned(); + } } } } diff --git a/src/extensions/SiteTreeLink.php b/src/extensions/SiteTreeLink.php index 2841da9..a9aa97a 100644 --- a/src/extensions/SiteTreeLink.php +++ b/src/extensions/SiteTreeLink.php @@ -3,6 +3,8 @@ namespace gorriecoe\Link\Extensions; use gorriecoe\Link\Models\Link; +use SilverStripe\CMS\Model\SiteTree; +use SilverStripe\Core\Config\Config; use SilverStripe\Core\Extension; /** @@ -20,7 +22,7 @@ public function onBeforeDuplicate() { $owner = $this->getOwner(); //loop through has_one relationships and reset any Link fields - if ($hasOne = $owner->Config()->get('has_one')) { + if (class_exists(SiteTree::class) && ($owner instanceof SiteTree) && ($hasOne = Config::inst()->get($owner::class, 'has_one'))) { foreach ($hasOne as $field => $fieldType) { if ($fieldType === Link::class) { $owner->{$field.'ID'} = 0; From 203740dfdb741c5a8f7a391e1c4f62ead64afa5a Mon Sep 17 00:00:00 2001 From: JamesDPC <69664712+JamesDPC@users.noreply.github.com> Date: Mon, 27 Oct 2025 06:02:08 +0000 Subject: [PATCH 06/22] [rector] Automated updates generated by rector configuration --- src/models/Link.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/models/Link.php b/src/models/Link.php index 4f1c170..e70ed5e 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -295,7 +295,7 @@ public function validate(): ValidationResult if ($this->{$type} == '') { $valid = false; $message = _t( - self::class . '.VALIDATIONERROR_EMPTY'.strtoupper($type), + self::class . '.VALIDATIONERROR_EMPTY'.strtoupper((string) $type), 'You must enter a {TypeLabel}', [ 'TypeLabel' => $this->TypeLabel @@ -325,7 +325,7 @@ public function validate(): ValidationResult switch ($type) { case 'URL': $allowedFirst = ['#', '/']; - if (!in_array(substr($this->URL, 0, 1), $allowedFirst) && !filter_var($this->URL, FILTER_VALIDATE_URL)) { + if (!in_array(substr((string) $this->URL, 0, 1), $allowedFirst) && !filter_var($this->URL, FILTER_VALIDATE_URL)) { $valid = false; $message = _t( self::class . '.VALIDATIONERROR_VALIDURL', @@ -345,7 +345,7 @@ public function validate(): ValidationResult break; case 'Phone': - if (!preg_match("/^\+?[0-9a-zA-Z\-\s]*[\,\#]?[0-9\-\s]*$/", $this->Phone)) { + if (!preg_match("/^\+?[0-9a-zA-Z\-\s]*[\,\#]?[0-9\-\s]*$/", (string) $this->Phone)) { $valid = false; $message = _t( self::class . '.VALIDATIONERROR_VALIDPHONE', @@ -442,9 +442,8 @@ public function addExtraClass($class): static /** * This is an alias to {@link addExtraClass()} * @param string $class CSS classes. - * @return Link */ - public function setClass($class) + public function setClass($class): static { return $this->addExtraClass($class); } @@ -780,7 +779,7 @@ public function LinkingMode() public function getTypeLabel() { $types = $this->config()->get('types'); - return isset($types[$this->Type]) ? _t(self::class . '.TYPE' . strtoupper($this->Type), $types[$this->Type]) : null; + return isset($types[$this->Type]) ? _t(self::class . '.TYPE' . strtoupper((string) $this->Type), $types[$this->Type]) : null; } /** From b2476e0d657415f834f84325f6b5820bf50a892f Mon Sep 17 00:00:00 2001 From: JamesDPC <69664712+JamesDPC@users.noreply.github.com> Date: Mon, 27 Oct 2025 06:02:09 +0000 Subject: [PATCH 07/22] [php-cs-fixer] Automated updates generated by php-cs-fixer configuration --- src/extensions/DefineableMarkupID.php | 2 +- src/extensions/LinkSiteTree.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/extensions/DefineableMarkupID.php b/src/extensions/DefineableMarkupID.php index ca1ace7..054e09b 100644 --- a/src/extensions/DefineableMarkupID.php +++ b/src/extensions/DefineableMarkupID.php @@ -46,7 +46,7 @@ public function updateCMSFields(FieldList $fields): FieldList public function onBeforeWrite() { $owner = $this->getOwner(); - if($owner instanceof Link) { + if ($owner instanceof Link) { $owner->IDCustomValue = Convert::raw2url($owner->IDCustomValue ?? ''); } } diff --git a/src/extensions/LinkSiteTree.php b/src/extensions/LinkSiteTree.php index 57fc2c0..c5d421d 100644 --- a/src/extensions/LinkSiteTree.php +++ b/src/extensions/LinkSiteTree.php @@ -63,7 +63,7 @@ public function updateCMSFields(FieldList $fields) { $owner = $this->getOwner(); if (class_exists(SiteTree::class) && ($owner instanceof Link)) { - + $sitetree_field_label = Config::inst()->get($owner::class, 'sitetree_field_label') ?: 'MenuTitle'; // Insert site tree field after the file selection field @@ -102,7 +102,7 @@ class_exists(SiteTree::class) && $owner->Type == 'SiteTree' ) { $currentPage = $owner->getCurrentPage(); - if($currentPage instanceof SiteTree) { + if ($currentPage instanceof SiteTree) { $status = $currentPage === $owner->SiteTree() || $currentPage->ID === $owner->SiteTreeID; } } @@ -117,7 +117,7 @@ class_exists(SiteTree::class) && $owner->Type == 'SiteTree' ) { $currentPage = $owner->getCurrentPage(); - if($currentPage instanceof SiteTree) { + if ($currentPage instanceof SiteTree) { $status = $owner->isCurrent() || in_array($owner->SiteTreeID, $currentPage->getAncestors()->column()); } } @@ -132,7 +132,7 @@ class_exists(SiteTree::class) && $owner->Type == 'SiteTree' ) { $currentPage = $owner->getCurrentPage(); - if($currentPage instanceof SiteTree) { + if ($currentPage instanceof SiteTree) { // Always false for root pages if (empty($owner->SiteTree()->ParentID)) { $status = false; From 2fcb2bd1b2b97b6913e79064ba88949538de8ab1 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:11:21 +1100 Subject: [PATCH 08/22] Do not return fields --- src/extensions/DefineableMarkupID.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/extensions/DefineableMarkupID.php b/src/extensions/DefineableMarkupID.php index 054e09b..b1c535a 100644 --- a/src/extensions/DefineableMarkupID.php +++ b/src/extensions/DefineableMarkupID.php @@ -27,7 +27,7 @@ class DefineableMarkupID extends Extension /** * Update Fields */ - public function updateCMSFields(FieldList $fields): FieldList + public function updateCMSFields(FieldList $fields) { $fields->addFieldToTab( 'Root.Main', @@ -37,7 +37,6 @@ public function updateCMSFields(FieldList $fields): FieldList ) ->setDescription(_t(self::class . '.IDCUSTOMVALUE', 'Define an ID for the link. This is particularly useful for google tracking.')) ); - return $fields; } /** From 10ecfcbec1a0386f46cb2109a764f864b8bb714b Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:46:43 +1100 Subject: [PATCH 09/22] Remove casting, convert value --- src/models/Link.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/models/Link.php b/src/models/Link.php index e70ed5e..14c6c60 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -113,16 +113,6 @@ class Link extends DataObject */ private static $allowed_types; - /** - * Ensures that the methods are wrapped in the correct type and - * values are safely escaped while rendering in the template. - */ - private static array $casting = [ - 'ClassAttr' => 'HTMLFragment', - 'TargetAttr' => 'HTMLFragment', - 'IDAttr' => 'HTMLFragment' - ]; - /** * @config */ @@ -663,7 +653,7 @@ public function getIDAttr(): string { $idValue = trim($this->getIDValue() ?? ''); if ($idValue !== '') { - return ' id="' . $idValue . '"'; + return ' id="' . Convert::raw2htmlatt($idValue) . '"'; } else { return ''; } From 8bbb7be0a1d73a90287b75aba55b93c4b0226e59 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:47:40 +1100 Subject: [PATCH 10/22] Remove graphsql methods and configuration --- src/models/Link.php | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/src/models/Link.php b/src/models/Link.php index 14c6c60..a1bafa1 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -134,17 +134,6 @@ class Link extends DataObject */ private static bool $link_to_folders = false; - /** - * Provides a quick way to define additional methods for provideGraphQLScaffolding as Fields - * @return Array - */ - private static array $gql_fields = []; - - /** - * Provides a quick way to define additional methods for provideGraphQLScaffolding as Nested Queries - */ - private static array $gql_nested_queries = []; - /** * Custom CSS classes for template */ @@ -394,27 +383,6 @@ public function onBeforeWrite() } } - /** - * Provides a quick way to define additional methods to provideGraphQLScaffolding as Fields - */ - public function gqlFields(): array - { - $fields = $this->config()->get('gql_fields'); - $this->extend('updateGqlFields', $fields); - return array_merge(['LinkURL'], $fields); - } - - /** - * Provides a quick way to define additional methods to provideGraphQLScaffolding as Nested Queries - * @return Array - */ - public function gqlNestedQueries() - { - $nested = $this->config()->get('gql_nested_queries'); - $this->extend('updateGqlNestedQueries', $nested); - return $nested; - } - /** * Set CSS classes for templates * @param string $class CSS classes. From cd4de96defb242cbfe317f9dab3dbf135284e4de Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:35:52 +1100 Subject: [PATCH 11/22] (enh) update template variable --- templates/Includes/Link.ss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/Includes/Link.ss b/templates/Includes/Link.ss index 1ae5282..a8a3bdc 100644 --- a/templates/Includes/Link.ss +++ b/templates/Includes/Link.ss @@ -1,4 +1,4 @@ -<% if LinkURL %> +<% if $LinkURL %> {$Title} From 79b5cdc4701ededcc71fda7e95aea323f139ace5 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:37:12 +1100 Subject: [PATCH 12/22] Return template variable methods as an HTMLFragment --- src/models/Link.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/models/Link.php b/src/models/Link.php index a1bafa1..fe74875 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -15,6 +15,8 @@ use SilverStripe\Forms\TextField; use SilverStripe\Forms\TreeDropdownField; use SilverStripe\ORM\DataObject; +use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\Core\Convert; use SilverStripe\Core\Validation\ValidationResult; use SilverStripe\Control\Director; @@ -588,6 +590,14 @@ public function getClassAttr(): string } } + /** + * Returns the html class attribute for the template variable $ClassAttr + */ + public function ClassAttr(): DBHTMLText + { + return DBField::create_field('HTMLFragment', $this->getClassAttr()); + } + /** * Returns the html target attribute */ @@ -604,6 +614,14 @@ public function getTargetAttr(): string return $this->OpenInNewWindow ? ' target="_blank" rel="noopener"' : ''; } + /** + * Returns the html target attribute for the template variable $TargetAttr + */ + public function TargetAttr(): DBHTMLText + { + return DBField::create_field('HTMLFragment', $this->getTargetAttr()); + } + /** * Returns the html id attribute */ @@ -627,6 +645,14 @@ public function getIDAttr(): string } } + /** + * Returns the html id attribute for the template variable $IDAttr + */ + public function IDAttr(): DBHTMLText + { + return DBField::create_field('HTMLFragment', $this->getIDAttr()); + } + /** * Returns the current page scope */ From 6ee097f1fc40fba66cc47598cf0d6fa501757f36 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:32:29 +1100 Subject: [PATCH 13/22] Add some tests and update handling based on failed tests --- src/extensions/DBStringLink.php | 4 +- src/models/Link.php | 18 ++++- src/view/Phone.php | 2 +- templates/Includes/Link.ss | 6 +- tests/.gitkeep | 0 tests/EmailLinkTest.php | 101 +++++++++++++++++++++++++ tests/PhoneLinkTest.php | 129 ++++++++++++++++++++++++++++++++ tests/UrlLinkTest.php | 101 +++++++++++++++++++++++++ 8 files changed, 350 insertions(+), 11 deletions(-) delete mode 100644 tests/.gitkeep create mode 100644 tests/EmailLinkTest.php create mode 100644 tests/PhoneLinkTest.php create mode 100644 tests/UrlLinkTest.php diff --git a/src/extensions/DBStringLink.php b/src/extensions/DBStringLink.php index c4e4844..4b01ca5 100644 --- a/src/extensions/DBStringLink.php +++ b/src/extensions/DBStringLink.php @@ -33,13 +33,13 @@ public function URLFriendly(): string /** * Provides string replace to allow phone number friendly urls */ - public function PhoneFriendly(): string + public function PhoneFriendly(): ?Phone { $value = $this->getOwner()->value; if ($value) { return Phone::create($value); } else { - return ''; + return null; } } } diff --git a/src/models/Link.php b/src/models/Link.php index fe74875..d023a93 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -2,6 +2,7 @@ namespace gorriecoe\Link\Models; +use gorriecoe\Link\View\Phone; use gorriecoe\Link\Extensions\LinkSiteTree; use InvalidArgumentException; use SilverStripe\Assets\File; @@ -409,7 +410,8 @@ public function setClass($class): static } /** - * Set style used for + * Set style class used in the class attribute. + * This is not used as an inline style attribute. * @param string $style */ public function setStyle($style): static @@ -507,6 +509,16 @@ public function geti18nStyles(): array return $i18nStyles; } + public function getFormattedPhoneLink(): string + { + $phone = $this->obj('Phone')->PhoneFriendly(); + if($phone instanceof Phone) { + return $phone->RFC3966()->forTemplate(); + } else { + return ''; + } + } + /** * Works out what the URL for this link should be based on it's Type */ @@ -525,7 +537,7 @@ public function getLinkURL(): ?string $LinkURL = $this->Email ? 'mailto:' . $this->Email : null; break; case 'Phone': - $LinkURL = $this->obj('Phone')->PhoneFriendly()->RFC3966(); + $LinkURL = $this->getFormattedPhoneLink(); break; case 'File': case 'SiteTree': @@ -784,7 +796,7 @@ public function forTemplate(): string { $link = ''; if ($this->LinkURL) { - $link = $this->renderWith($this->RenderTemplates); + $link = $this->renderWith($this->getRenderTemplates()); } $this->extend('updateTemplate', $link); diff --git a/src/view/Phone.php b/src/view/Phone.php index b53429b..bb4c43f 100644 --- a/src/view/Phone.php +++ b/src/view/Phone.php @@ -20,7 +20,7 @@ class Phone extends ModelData /** * The country the user is dialing from. */ - protected string $fromCountry; + protected string $fromCountry = ''; private static string $default_country = 'NZ'; diff --git a/templates/Includes/Link.ss b/templates/Includes/Link.ss index a8a3bdc..4918001 100644 --- a/templates/Includes/Link.ss +++ b/templates/Includes/Link.ss @@ -1,5 +1 @@ -<% if $LinkURL %> - - {$Title} - -<% end_if %> +<% if $LinkURL %>{$Title}<% end_if %> diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/EmailLinkTest.php b/tests/EmailLinkTest.php new file mode 100644 index 0000000..6f23096 --- /dev/null +++ b/tests/EmailLinkTest.php @@ -0,0 +1,101 @@ + "Email \"> me", + 'Type' => 'Email', + 'Email' => 'test@example.com', + 'OpenInNewWindow' => false + ]); + $link->write(); + + $this->assertEquals( + 'Email "> me', + trim($link->forTemplate()) + ); + } + + public function testLinkWithTargetAttribute(): void + { + $link = Link::create([ + 'Type' => 'Email', + 'Email' => 'test@example.com', + 'OpenInNewWindow' => true + ]); + $link->write(); + + $this->assertEquals( + 'test@example.com', + trim($link->forTemplate()) + ); + } + + public function testLinkWithClassAttribute(): void + { + $link = Link::create([ + 'Type' => 'Email', + 'Email' => 'test@example.com', + 'OpenInNewWindow' => false + ]); + $link->write(); + // a style class + $link->setStyle("link-set-style"); + // a class + $link->setClass("link-set-class"); + // multi classes via extra class + $link->addExtraClass("link-extra-class-one link-extra-class-two"); + + $this->assertEquals( + 'test@example.com', + trim($link->forTemplate()) + ); + } + + public function testLinkWithEscapedClassAttribute(): void + { + $link = Link::create([ + 'Type' => 'Email', + 'Email' => 'test@example.com', + 'OpenInNewWindow' => false + ]); + $link->write(); + $link->setClass("\">strongassertEquals( + 'test@example.com', + trim($link->forTemplate()) + ); + } + + public function testLinkWithAttributes(): void + { + $link = Link::create([ + 'Type' => 'Email', + 'Email' => 'test@example.com', + 'OpenInNewWindow' => true + ]); + $link->write(); + // a style class + $link->setStyle("link-set-style"); + // a class + $link->setClass("link-set-class"); + // multi classes via extra class + $link->addExtraClass("link-extra-class-one link-extra-class-two"); + + $this->assertEquals( + 'test@example.com', + trim($link->forTemplate()) + ); + } +} \ No newline at end of file diff --git a/tests/PhoneLinkTest.php b/tests/PhoneLinkTest.php new file mode 100644 index 0000000..055ea32 --- /dev/null +++ b/tests/PhoneLinkTest.php @@ -0,0 +1,129 @@ + "Phone \"> me", + 'Type' => 'Phone', + 'Phone' => '+6480074992488', + 'OpenInNewWindow' => false + ]); + $link->write(); + + $this->assertEquals( + 'Phone "> me', + trim($link->forTemplate()) + ); + } + + public function testLinkWithTargetAttribute(): void + { + $link = Link::create([ + 'Type' => 'Phone', + 'Phone' => '+6480074992488', + 'OpenInNewWindow' => true + ]); + $link->write(); + + $this->assertEquals( + '+6480074992488', + trim($link->forTemplate()) + ); + } + + public function testLinkWithClassAttribute(): void + { + $link = Link::create([ + 'Type' => 'Phone', + 'Phone' => '+6480074992488', + 'OpenInNewWindow' => false + ]); + $link->write(); + // a style class + $link->setStyle("link-set-style"); + // a class + $link->setClass("link-set-class"); + // multi classes via extra class + $link->addExtraClass("link-extra-class-one link-extra-class-two"); + + $this->assertEquals( + '+6480074992488', + trim($link->forTemplate()) + ); + } + + public function testLinkWithEscapedClassAttribute(): void + { + $link = Link::create([ + 'Type' => 'Phone', + 'Phone' => '+6480074992488', + 'OpenInNewWindow' => false + ]); + $link->write(); + $link->setClass("\">strongassertEquals( + '+6480074992488', + trim($link->forTemplate()) + ); + } + + public function testLinkWithAttributes(): void + { + $link = Link::create([ + 'Type' => 'Phone', + 'Phone' => '+6480074992488', + 'OpenInNewWindow' => true + ]); + $link->write(); + // a style class + $link->setStyle("link-set-style"); + // a class + $link->setClass("link-set-class"); + // multi classes via extra class + $link->addExtraClass("link-extra-class-one link-extra-class-two"); + + $this->assertEquals( + '+6480074992488', + trim($link->forTemplate()) + ); + } + + public function testPhoneFormats(): void + { + $link = Link::create([ + 'Type' => 'Phone', + 'Phone' => '+6480074992488', + 'OpenInNewWindow' => false + ]); + $link->write(); + + $obj = $link->obj('Phone'); + $phoneFriendly = $obj->PhoneFriendly(); + $this->assertInstanceof(Phone::class, $phoneFriendly); + + $e164 = $phoneFriendly->E164(); + $this->assertEquals('+6480074992488', $e164->forTemplate()); + + $national = $phoneFriendly->National(); + $this->assertEquals('80074992488', $national->forTemplate()); + + $international = $phoneFriendly->International(); + $this->assertEquals('+64 80074992488', $international->forTemplate()); + + $rfc3966 = $phoneFriendly->RFC3966(); + $this->assertEquals('tel:+64-80074992488', $rfc3966->forTemplate()); + + } +} \ No newline at end of file diff --git a/tests/UrlLinkTest.php b/tests/UrlLinkTest.php new file mode 100644 index 0000000..169e07c --- /dev/null +++ b/tests/UrlLinkTest.php @@ -0,0 +1,101 @@ + "Example \"> link", + 'Type' => 'URL', + 'URL' => 'https://example.com', + 'OpenInNewWindow' => false + ]); + $link->write(); + + $this->assertEquals( + 'Example "> link', + trim($link->forTemplate()) + ); + } + + public function testLinkWithTargetAttribute(): void + { + $link = Link::create([ + 'Type' => 'URL', + 'URL' => 'https://example.com', + 'OpenInNewWindow' => true + ]); + $link->write(); + + $this->assertEquals( + 'https://example.com', + trim($link->forTemplate()) + ); + } + + public function testLinkWithClassAttribute(): void + { + $link = Link::create([ + 'Type' => 'URL', + 'URL' => 'https://example.com', + 'OpenInNewWindow' => false + ]); + $link->write(); + // a style class + $link->setStyle("link-set-style"); + // a class + $link->setClass("link-set-class"); + // multi classes via extra class + $link->addExtraClass("link-extra-class-one link-extra-class-two"); + + $this->assertEquals( + 'https://example.com', + trim($link->forTemplate()) + ); + } + + public function testLinkWithEscapedClassAttribute(): void + { + $link = Link::create([ + 'Type' => 'URL', + 'URL' => 'https://example.com', + 'OpenInNewWindow' => false + ]); + $link->write(); + $link->setClass("\">strongassertEquals( + 'https://example.com', + trim($link->forTemplate()) + ); + } + + public function testLinkWithAttributes(): void + { + $link = Link::create([ + 'Type' => 'URL', + 'URL' => 'https://example.com', + 'OpenInNewWindow' => true + ]); + $link->write(); + // a style class + $link->setStyle("link-set-style"); + // a class + $link->setClass("link-set-class"); + // multi classes via extra class + $link->addExtraClass("link-extra-class-one link-extra-class-two"); + + $this->assertEquals( + 'https://example.com', + trim($link->forTemplate()) + ); + } +} \ No newline at end of file From 8f20280f26180ca91d5975c1aab356377433e653 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Tue, 28 Oct 2025 11:22:14 +1100 Subject: [PATCH 14/22] Add File and SiteTree tests --- tests/FileLinkTest.php | 49 ++++++++++++++++++++++++++++++++++++ tests/SiteTreeLinkTest.php | 51 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 tests/FileLinkTest.php create mode 100644 tests/SiteTreeLinkTest.php diff --git a/tests/FileLinkTest.php b/tests/FileLinkTest.php new file mode 100644 index 0000000..674cbae --- /dev/null +++ b/tests/FileLinkTest.php @@ -0,0 +1,49 @@ + 'FileTest.txt', + 'FileHash' => '55b443b60176235ef09801153cca4e6da7494a0c', + 'Name' => 'FileTest.txt' + ]); + $file->setFromString(str_repeat('x', 1000000), $file->getFilename()); + $file->write(); + $file->publishSingle(); + $fileLink = $file->Link(); + $this->assertNotEmpty($fileLink); + + $link = Link::create([ + 'Title' => "Download \"> file", + 'Type' => 'File', + 'FileID' => $file->ID, + 'OpenInNewWindow' => false + ]); + $link->write(); + + $this->assertEquals( + 'Download "> file', + trim($link->forTemplate()) + ); + } + +} \ No newline at end of file diff --git a/tests/SiteTreeLinkTest.php b/tests/SiteTreeLinkTest.php new file mode 100644 index 0000000..73e4f57 --- /dev/null +++ b/tests/SiteTreeLinkTest.php @@ -0,0 +1,51 @@ +markTestSkipped( + 'The silverstripe/cms module is required to run this test.' + ); + } + } + + public function testLink(): void + { + + $siteTree = SiteTree::create([ + 'Title' => 'Test page', + 'URLSegment' => 'test-page', + 'ParentID' => 0 + ]); + $siteTree->write(); + $siteTree->publishSingle(); + $siteTreeLink = $siteTree->Link(); + $this->assertNotEmpty($siteTreeLink); + + $link = Link::create([ + 'Title' => "Visit \"> page", + 'Type' => 'SiteTree', + 'SiteTreeID' => $siteTree->ID, + 'OpenInNewWindow' => false + ]); + $link->write(); + + $this->assertEquals( + 'Visit "> page', + trim($link->forTemplate()) + ); + } + +} \ No newline at end of file From 8eddd8ac25cbb8da679c9e46ab6bf07939e04399 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Tue, 28 Oct 2025 11:23:10 +1100 Subject: [PATCH 15/22] Update template methods --- src/models/Link.php | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/models/Link.php b/src/models/Link.php index d023a93..e4153f8 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -569,6 +569,14 @@ public function getLinkURL(): ?string return $LinkURL; } + /** + * Returns value for the template variable $LinkURL + */ + public function LinkURL(): string + { + return $this->getLinkURL(); + } + /** * Returns the css classes */ @@ -589,6 +597,14 @@ public function getClass(): string return ''; } + /** + * Returns the html class attribute value for the template variable $Class + */ + public function Class(): string + { + return $this->getClass(); + } + /** * Returns the html class attribute */ @@ -618,6 +634,14 @@ public function getTarget(): string return $this->OpenInNewWindow ? "_blank" : ''; } + /** + * Returns the html target attribute value for the template variable $Target + */ + public function Target(): string + { + return $this->getTarget(); + } + /** * Returns the html target attribute */ @@ -644,6 +668,14 @@ public function getIDValue(): ?string return $id; } + /** + * Returns the html id attribute value for the template variable $IDValue + */ + public function IDValue(): ?string + { + return $this->getIDValue(); + } + /** * Renders an HTML ID attribute */ From 5b4272bab701b6fcaa9eb861c54d7b3d0f25340d Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Tue, 28 Oct 2025 11:37:54 +1100 Subject: [PATCH 16/22] Use method name --- src/models/Link.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/Link.php b/src/models/Link.php index e4153f8..9999b8b 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -827,7 +827,7 @@ public function baseClassName($class): string public function forTemplate(): string { $link = ''; - if ($this->LinkURL) { + if ($this->getLinkURL()) { $link = $this->renderWith($this->getRenderTemplates()); } From d741f1d0a787b73b03cd31d3f910b54b8e8feeb2 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:26:10 +1100 Subject: [PATCH 17/22] Updates based on analysis --- src/models/Link.php | 3 +++ tests/FileLinkTest.php | 4 ++-- tests/SiteTreeLinkTest.php | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/models/Link.php b/src/models/Link.php index 9999b8b..ebf262c 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -623,6 +623,7 @@ public function getClassAttr(): string */ public function ClassAttr(): DBHTMLText { + // @phpstan-ignore return.type return DBField::create_field('HTMLFragment', $this->getClassAttr()); } @@ -655,6 +656,7 @@ public function getTargetAttr(): string */ public function TargetAttr(): DBHTMLText { + // @phpstan-ignore return.type return DBField::create_field('HTMLFragment', $this->getTargetAttr()); } @@ -694,6 +696,7 @@ public function getIDAttr(): string */ public function IDAttr(): DBHTMLText { + // @phpstan-ignore return.type return DBField::create_field('HTMLFragment', $this->getIDAttr()); } diff --git a/tests/FileLinkTest.php b/tests/FileLinkTest.php index 674cbae..d8ddeee 100644 --- a/tests/FileLinkTest.php +++ b/tests/FileLinkTest.php @@ -4,7 +4,7 @@ use gorriecoe\Link\Models\Link; use SilverStripe\Assets\File; -use Silverstripe\Assets\Dev\TestAssetStore; +use SilverStripe\Assets\Dev\TestAssetStore; use SilverStripe\Dev\SapphireTest; class FileLinkTest extends SapphireTest @@ -28,7 +28,7 @@ public function testLink(): void ]); $file->setFromString(str_repeat('x', 1000000), $file->getFilename()); $file->write(); - $file->publishSingle(); + $file->publishFile(); $fileLink = $file->Link(); $this->assertNotEmpty($fileLink); diff --git a/tests/SiteTreeLinkTest.php b/tests/SiteTreeLinkTest.php index 73e4f57..f27e50c 100644 --- a/tests/SiteTreeLinkTest.php +++ b/tests/SiteTreeLinkTest.php @@ -24,6 +24,12 @@ protected function setUp(): void public function testLink(): void { + if (!class_exists(SiteTree::class)) { + $this->markTestSkipped( + 'The silverstripe/cms module is required to run this test.' + ); + } + $siteTree = SiteTree::create([ 'Title' => 'Test page', 'URLSegment' => 'test-page', From b90dfcefd0c031ca8db00ac183639de0937aa3ca Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:32:15 +1100 Subject: [PATCH 18/22] (dev) require phpstan/phpstan-phpunit --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0b5ec05..18ddad0 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,8 @@ "nswdpc/ci-files": "dev-v-4", "phpstan/phpstan": "^2", "phpunit/phpunit": "^11.5", - "rector/rector": "^2" + "rector/rector": "^2", + "phpstan/phpstan-phpunit": "^2" }, "extra": { "installer-name": "link" From 7f232f575eb467428e2919c2386abe186db33428 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:35:08 +1100 Subject: [PATCH 19/22] Update .gitattributes --- .gitattributes | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitattributes b/.gitattributes index dfe0770..51389f0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,12 @@ # Auto detect text files and perform LF normalization * text=auto +/tests export-ignore +/docs export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/code-of-conduct.md export-ignore +/contributing.md export-ignore +/README.md export-ignore +/.scrutinizer.yml export-ignore +/.github ` export-ignore From ff04ed3d3398e98e4ddcbbd5805c64fea3509b75 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:35:33 +1100 Subject: [PATCH 20/22] Update .gitattributes --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 51389f0..c846455 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,4 +9,4 @@ /contributing.md export-ignore /README.md export-ignore /.scrutinizer.yml export-ignore -/.github ` export-ignore +/.github export-ignore From 07e8e00cb4b78379a66003828ef01f85f23bfa7e Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:36:13 +1100 Subject: [PATCH 21/22] (php-cs-fixer) fix --- src/models/Link.php | 2 +- tests/EmailLinkTest.php | 3 +-- tests/FileLinkTest.php | 3 +-- tests/PhoneLinkTest.php | 3 +-- tests/SiteTreeLinkTest.php | 3 +-- tests/UrlLinkTest.php | 3 +-- 6 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/models/Link.php b/src/models/Link.php index ebf262c..e34fae4 100644 --- a/src/models/Link.php +++ b/src/models/Link.php @@ -512,7 +512,7 @@ public function geti18nStyles(): array public function getFormattedPhoneLink(): string { $phone = $this->obj('Phone')->PhoneFriendly(); - if($phone instanceof Phone) { + if ($phone instanceof Phone) { return $phone->RFC3966()->forTemplate(); } else { return ''; diff --git a/tests/EmailLinkTest.php b/tests/EmailLinkTest.php index 6f23096..b13852a 100644 --- a/tests/EmailLinkTest.php +++ b/tests/EmailLinkTest.php @@ -7,7 +7,6 @@ class EmailLinkTest extends SapphireTest { - protected $usesDatabase = true; public function testLinkWithNoAttributes(): void @@ -98,4 +97,4 @@ public function testLinkWithAttributes(): void trim($link->forTemplate()) ); } -} \ No newline at end of file +} diff --git a/tests/FileLinkTest.php b/tests/FileLinkTest.php index d8ddeee..9483287 100644 --- a/tests/FileLinkTest.php +++ b/tests/FileLinkTest.php @@ -9,7 +9,6 @@ class FileLinkTest extends SapphireTest { - protected $usesDatabase = true; protected function setUp(): void @@ -46,4 +45,4 @@ public function testLink(): void ); } -} \ No newline at end of file +} diff --git a/tests/PhoneLinkTest.php b/tests/PhoneLinkTest.php index 055ea32..ad171b1 100644 --- a/tests/PhoneLinkTest.php +++ b/tests/PhoneLinkTest.php @@ -8,7 +8,6 @@ class PhoneLinkTest extends SapphireTest { - protected $usesDatabase = true; public function testLinkWithNoAttributes(): void @@ -126,4 +125,4 @@ public function testPhoneFormats(): void $this->assertEquals('tel:+64-80074992488', $rfc3966->forTemplate()); } -} \ No newline at end of file +} diff --git a/tests/SiteTreeLinkTest.php b/tests/SiteTreeLinkTest.php index f27e50c..bd4e9ff 100644 --- a/tests/SiteTreeLinkTest.php +++ b/tests/SiteTreeLinkTest.php @@ -8,7 +8,6 @@ class SiteTreeLinkTest extends SapphireTest { - protected $usesDatabase = true; protected function setUp(): void @@ -54,4 +53,4 @@ public function testLink(): void ); } -} \ No newline at end of file +} diff --git a/tests/UrlLinkTest.php b/tests/UrlLinkTest.php index 169e07c..e34d916 100644 --- a/tests/UrlLinkTest.php +++ b/tests/UrlLinkTest.php @@ -7,7 +7,6 @@ class UrlLinkTest extends SapphireTest { - protected $usesDatabase = true; public function testLinkWithNoAttributes(): void @@ -98,4 +97,4 @@ public function testLinkWithAttributes(): void trim($link->forTemplate()) ); } -} \ No newline at end of file +} From 6d1dcb2762f04afa6e89fbce0af8013efea5d954 Mon Sep 17 00:00:00 2001 From: "James (PD)" <69664712+JamesDPC@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:13:30 +1100 Subject: [PATCH 22/22] (rector) process --- tests/EmailLinkTest.php | 4 ++-- tests/FileLinkTest.php | 4 +++- tests/PhoneLinkTest.php | 4 ++-- tests/SiteTreeLinkTest.php | 4 +++- tests/UrlLinkTest.php | 4 ++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/EmailLinkTest.php b/tests/EmailLinkTest.php index b13852a..0ed5551 100644 --- a/tests/EmailLinkTest.php +++ b/tests/EmailLinkTest.php @@ -12,7 +12,7 @@ class EmailLinkTest extends SapphireTest public function testLinkWithNoAttributes(): void { $link = Link::create([ - 'Title' => "Email \"> me", + 'Title' => 'Email "> me', 'Type' => 'Email', 'Email' => 'test@example.com', 'OpenInNewWindow' => false @@ -69,7 +69,7 @@ public function testLinkWithEscapedClassAttribute(): void 'OpenInNewWindow' => false ]); $link->write(); - $link->setClass("\">strongsetClass('">strongassertEquals( 'test@example.com', diff --git a/tests/FileLinkTest.php b/tests/FileLinkTest.php index 9483287..66b8706 100644 --- a/tests/FileLinkTest.php +++ b/tests/FileLinkTest.php @@ -11,6 +11,7 @@ class FileLinkTest extends SapphireTest { protected $usesDatabase = true; + #[\Override] protected function setUp(): void { parent::setUp(); @@ -28,11 +29,12 @@ public function testLink(): void $file->setFromString(str_repeat('x', 1000000), $file->getFilename()); $file->write(); $file->publishFile(); + $fileLink = $file->Link(); $this->assertNotEmpty($fileLink); $link = Link::create([ - 'Title' => "Download \"> file", + 'Title' => 'Download "> file', 'Type' => 'File', 'FileID' => $file->ID, 'OpenInNewWindow' => false diff --git a/tests/PhoneLinkTest.php b/tests/PhoneLinkTest.php index ad171b1..a970093 100644 --- a/tests/PhoneLinkTest.php +++ b/tests/PhoneLinkTest.php @@ -13,7 +13,7 @@ class PhoneLinkTest extends SapphireTest public function testLinkWithNoAttributes(): void { $link = Link::create([ - 'Title' => "Phone \"> me", + 'Title' => 'Phone "> me', 'Type' => 'Phone', 'Phone' => '+6480074992488', 'OpenInNewWindow' => false @@ -70,7 +70,7 @@ public function testLinkWithEscapedClassAttribute(): void 'OpenInNewWindow' => false ]); $link->write(); - $link->setClass("\">strongsetClass('">strongassertEquals( '+6480074992488', diff --git a/tests/SiteTreeLinkTest.php b/tests/SiteTreeLinkTest.php index bd4e9ff..bf483ac 100644 --- a/tests/SiteTreeLinkTest.php +++ b/tests/SiteTreeLinkTest.php @@ -10,6 +10,7 @@ class SiteTreeLinkTest extends SapphireTest { protected $usesDatabase = true; + #[\Override] protected function setUp(): void { parent::setUp(); @@ -36,11 +37,12 @@ public function testLink(): void ]); $siteTree->write(); $siteTree->publishSingle(); + $siteTreeLink = $siteTree->Link(); $this->assertNotEmpty($siteTreeLink); $link = Link::create([ - 'Title' => "Visit \"> page", + 'Title' => 'Visit "> page', 'Type' => 'SiteTree', 'SiteTreeID' => $siteTree->ID, 'OpenInNewWindow' => false diff --git a/tests/UrlLinkTest.php b/tests/UrlLinkTest.php index e34d916..5be9d59 100644 --- a/tests/UrlLinkTest.php +++ b/tests/UrlLinkTest.php @@ -12,7 +12,7 @@ class UrlLinkTest extends SapphireTest public function testLinkWithNoAttributes(): void { $link = Link::create([ - 'Title' => "Example \"> link", + 'Title' => 'Example "> link', 'Type' => 'URL', 'URL' => 'https://example.com', 'OpenInNewWindow' => false @@ -69,7 +69,7 @@ public function testLinkWithEscapedClassAttribute(): void 'OpenInNewWindow' => false ]); $link->write(); - $link->setClass("\">strongsetClass('">strongassertEquals( 'https://example.com',