@@ -25,21 +25,134 @@ class CacheTag extends ConfigurableRevalidatorBase implements RevalidatorInterfa
2525 * {@inheritdoc}
2626 */
2727 public function defaultConfiguration () {
28- return [];
28+ return [
29+ 'entity_tag ' => TRUE ,
30+ 'entity_list_tag ' => TRUE ,
31+ 'additional_tags ' => NULL ,
32+ ];
2933 }
3034
3135 /**
3236 * {@inheritdoc}
3337 */
3438 public function buildConfigurationForm (array $ form , FormStateInterface $ form_state ) {
39+ // Get entity type and bundle from form callback object.
40+ $ entity_type_id = NULL ;
41+ $ bundle = NULL ;
42+
43+ try {
44+ $ entity_bundle_string = $ form_state ->getBuildInfo ()['callback_object ' ]->getEntity ()->id ();
45+ // Split "node.quote" into entity type and bundle.
46+ if (strpos ($ entity_bundle_string , '. ' ) !== FALSE ) {
47+ [$ entity_type_id , $ bundle ] = explode ('. ' , $ entity_bundle_string , 2 );
48+ }
49+ }
50+ catch (\Exception $ exception ) {
51+ // Fallback if we can't get the entity info.
52+ Error::logException ($ this ->logger , $ exception );
53+ $ entity_type_id = NULL ;
54+ $ bundle = NULL ;
55+ }
56+
57+ $ form ['entity_tag ' ] = [
58+ '#title ' => $ this ->t ('Revalidate entity cache tag ' ),
59+ '#description ' => $ this ->t ('Revalidate pages with the individual entity cache tag (e.g., @entity_type:123). ' , [
60+ '@entity_type ' => $ entity_type_id ?: 'node ' ,
61+ ]),
62+ '#type ' => 'checkbox ' ,
63+ '#default_value ' => $ this ->configuration ['entity_tag ' ] ?? TRUE ,
64+ ];
65+
66+ // Generate specific label and description based on detected entity type.
67+ if ($ entity_type_id && $ bundle ) {
68+ if ($ entity_type_id === 'node ' ) {
69+ $ list_tag_example = 'node_list: ' . $ bundle ;
70+ $ next_js_example = 'tags: ["node_list: ' . $ bundle . '"] ' ;
71+ }
72+ elseif ($ entity_type_id === 'taxonomy_term ' ) {
73+ $ list_tag_example = 'taxonomy_term_list: ' . $ bundle ;
74+ $ next_js_example = 'tags: ["taxonomy_term_list: ' . $ bundle . '"] ' ;
75+ }
76+ else {
77+ $ list_tag_example = $ entity_type_id . '_list: ' . $ bundle ;
78+ $ next_js_example = 'tags: [" ' . $ entity_type_id . '_list: ' . $ bundle . '"] ' ;
79+ }
80+
81+ $ title = $ this ->t ('Revalidate @tag cache tags ' , ['@tag ' => $ list_tag_example ]);
82+ $ description = $ this ->t ('Revalidates pages tagged with @tag when @entity_type entities of type @bundle change.<br><br>In Next.js use: <code>@example</code> ' , [
83+ '@tag ' => $ list_tag_example ,
84+ '@entity_type ' => $ entity_type_id ,
85+ '@bundle ' => $ bundle ,
86+ '@example ' => $ next_js_example ,
87+ ]);
88+ }
89+ else {
90+ $ title = $ this ->t ('Revalidate [entity_type]_list:[bundle] cache tags ' );
91+ $ description = $ this ->t ('Revalidates pages tagged with entity type and bundle list cache tags when entities change.<br><strong>Node entities:</strong> generates node_list:[bundle] (e.g., node_list:article, node_list:person)<br><strong>Taxonomy terms:</strong> generates taxonomy_term_list:[vocabulary] (e.g., taxonomy_term_list:tags)<br><strong>Other entities:</strong> generates [entity_type]_list:[bundle]<br><br>In Next.js use: <code>tags: ["node_list:article"]</code> or <code>tags: ["taxonomy_term_list:tags"]</code> ' );
92+ }
93+
94+ $ form ['entity_list_tag ' ] = [
95+ '#title ' => $ title ,
96+ '#description ' => $ description ,
97+ '#type ' => 'checkbox ' ,
98+ '#default_value ' => $ this ->configuration ['entity_list_tag ' ] ?? TRUE ,
99+ ];
100+
101+ $ form ['additional_tags ' ] = [
102+ '#type ' => 'textarea ' ,
103+ '#title ' => $ this ->t ('Additional cache tags to revalidate ' ),
104+ '#default_value ' => $ this ->configuration ['additional_tags ' ] ?? '' ,
105+ '#description ' => $ this ->t ('Additional cache tags to revalidate when this entity type changes. Enter one tag per line. Examples:<br>node_list:all<br>search_results<br>homepage ' ),
106+ ];
107+
35108 return $ form ;
36109 }
37110
111+ /**
112+ * {@inheritdoc}
113+ */
114+ public function validateConfigurationForm (array &$ form , FormStateInterface $ form_state ) {
115+ $ additional_tags = $ form_state ->getValue ('additional_tags ' );
116+
117+ if (!empty ($ additional_tags )) {
118+ $ tags = array_map ('trim ' , explode ("\n" , $ additional_tags ));
119+ $ tags = array_filter ($ tags );
120+
121+ foreach ($ tags as $ tag ) {
122+ // Validate that each tag is a string and doesn't contain invalid
123+ // characters.
124+ if (!is_string ($ tag ) || empty ($ tag )) {
125+ $ form_state ->setErrorByName ('additional_tags ' , $ this ->t ('Each cache tag must be a non-empty string. ' ));
126+ break ;
127+ }
128+
129+ // Check for invalid characters (spaces, special characters that could
130+ // break cache tags).
131+ if (preg_match ('/[^\w\-:._]/ ' , $ tag )) {
132+ $ form_state ->setErrorByName ('additional_tags ' , $ this ->t ('Cache tags can only contain letters, numbers, hyphens, colons, periods, and underscores. Invalid tag: @tag ' , [
133+ '@tag ' => $ tag ,
134+ ]));
135+ break ;
136+ }
137+
138+ // Check for reasonable length limit.
139+ if (strlen ($ tag ) > 255 ) {
140+ $ form_state ->setErrorByName ('additional_tags ' , $ this ->t ('Cache tags must be 255 characters or less. Invalid tag: @tag ' , [
141+ '@tag ' => $ tag ,
142+ ]));
143+ break ;
144+ }
145+ }
146+ }
147+ }
148+
38149 /**
39150 * {@inheritdoc}
40151 */
41152 public function submitConfigurationForm (array &$ form , FormStateInterface $ form_state ) {
42- // No configuration form.
153+ $ this ->configuration ['entity_tag ' ] = $ form_state ->getValue ('entity_tag ' );
154+ $ this ->configuration ['entity_list_tag ' ] = $ form_state ->getValue ('entity_list_tag ' );
155+ $ this ->configuration ['additional_tags ' ] = $ form_state ->getValue ('additional_tags ' );
43156 }
44157
45158 /**
@@ -57,26 +170,55 @@ public function revalidate(EntityActionEvent $event): bool {
57170 return FALSE ;
58171 }
59172
60- // Get all available cache tags (including list tags).
61- $ list_tags = $ entity ->getEntityType ()->getListCacheTags ();
62- if ($ entity ->getEntityType ()->hasKey ('bundle ' )) {
63- $ list_tags [] = $ entity ->getEntityTypeId () . '_list: ' . $ entity ->bundle ();
173+ $ cache_tags = [];
174+
175+ // Add individual entity cache tags if enabled.
176+ if (!empty ($ this ->configuration ['entity_tag ' ])) {
177+ $ cache_tags = array_merge ($ cache_tags , $ entity ->getCacheTags ());
178+ }
179+
180+ // Add entity list cache tags if enabled.
181+ if (!empty ($ this ->configuration ['entity_list_tag ' ])) {
182+ $ list_tags = $ entity ->getEntityType ()->getListCacheTags ();
183+ if ($ entity ->getEntityType ()->hasKey ('bundle ' )) {
184+ $ list_tags [] = $ entity ->getEntityTypeId () . '_list: ' . $ entity ->bundle ();
185+ }
186+ $ cache_tags = array_merge ($ cache_tags , $ list_tags );
187+ }
188+
189+ // Add additional cache tags.
190+ if (!empty ($ this ->configuration ['additional_tags ' ])) {
191+ $ additional_tags = array_map ('trim ' , explode ("\n" , $ this ->configuration ['additional_tags ' ]));
192+ $ additional_tags = array_filter ($ additional_tags );
193+ $ cache_tags = array_merge ($ cache_tags , $ additional_tags );
194+ }
195+
196+ if (!count ($ cache_tags )) {
197+ if ($ this ->nextSettingsManager ->isDebug ()) {
198+ $ this ->logger ->debug ('No cache tags found for revalidation. Entity: @entity_type:@entity_id ' , [
199+ '@entity_type ' => $ entity ->getEntityTypeId (),
200+ '@entity_id ' => $ entity ->id (),
201+ ]);
202+ }
203+ return FALSE ;
64204 }
65- $ combined_tags = array_merge ($ entity ->getCacheTags (), $ list_tags );
66- $ cache_tags = implode (', ' , $ combined_tags );
205+
206+ // Remove duplicates and convert to comma-separated string.
207+ $ cache_tags = array_unique ($ cache_tags );
208+ $ cache_tags_string = implode (', ' , $ cache_tags );
67209
68210 /** @var \Drupal\next\Entity\NextSite $site */
69211 foreach ($ sites as $ site ) {
70212 try {
71- $ revalidate_url = $ site ->buildRevalidateUrl (['tags ' => $ cache_tags ]);
213+ $ revalidate_url = $ site ->buildRevalidateUrl (['tags ' => $ cache_tags_string ]);
72214 if (!$ revalidate_url ) {
73215 throw new \Exception ('No revalidate url set. ' );
74216 }
75217
76218 if ($ this ->nextSettingsManager ->isDebug ()) {
77219 $ this ->logger ->notice ('(@action): Revalidating tags %list for the site %site. URL: %url ' , [
78220 '@action ' => $ event ->getAction (),
79- '%list ' => $ cache_tags ,
221+ '%list ' => $ cache_tags_string ,
80222 '%site ' => $ site ->label (),
81223 '%url ' => $ revalidate_url ->toString (),
82224 ]);
@@ -87,14 +229,24 @@ public function revalidate(EntityActionEvent $event): bool {
87229 if ($ this ->nextSettingsManager ->isDebug ()) {
88230 $ this ->logger ->notice ('(@action): Successfully revalidated tags %list for the site %site. URL: %url ' , [
89231 '@action ' => $ event ->getAction (),
90- '%list ' => $ cache_tags ,
232+ '%list ' => $ cache_tags_string ,
91233 '%site ' => $ site ->label (),
92234 '%url ' => $ revalidate_url ->toString (),
93235 ]);
94236 }
95237
96238 $ revalidated = TRUE ;
97239 }
240+ else {
241+ $ status_code = $ response ? $ response ->getStatusCode () : 'unknown ' ;
242+ $ this ->logger ->warning ('(@action): Failed to revalidate tags %list for the site %site. HTTP status: %status. URL: %url ' , [
243+ '@action ' => $ event ->getAction (),
244+ '%list ' => $ cache_tags_string ,
245+ '%site ' => $ site ->label (),
246+ '%status ' => $ status_code ,
247+ '%url ' => $ revalidate_url ->toString (),
248+ ]);
249+ }
98250 }
99251 catch (\Exception $ exception ) {
100252 Error::logException ($ this ->logger , $ exception );
0 commit comments