Problem/Motivation
When the display setting "Restrict exceptions display to x days in future" is set to 0, and the current day is an exception day, PHP throws a fatal error:
Drupal\office_hours\Plugin\Field\FieldType\OfficeHoursItem::formatLabel(): Return value must be of type string, array returned in Drupal\office_hours\Plugin\Field\FieldType\OfficeHoursItem::formatLabel() (line 450 of modules/contrib/office_hours/src/Plugin/Field/FieldType/OfficeHoursItem.php).
This is because under those specific circumstances, $value['day'] is empty.
Steps to reproduce
- Set "Restrict exceptions display to x days in future" to 0
- Add an exception for the current day to the field
- Attempt to render and observe the error
- Change the exception to tomorrow
- Render and see the proper output
- Change the exception back to today
- Change "Restrict exceptions display to x days in future" to 1
- Render and see the proper output
Proposed resolution
If we prevent the error (and a few additional warnings) by wrapping in if (isset($value['day']) {} there's no glaring flaw with the output.
However, the current day's exception day is not listed. It definitely should -- but that can be for later.
Why it's empty can also be a question for later. I suspect a larger fix would have something to do with keepExceptionDaysInHorizon or its results, since that's the only place $settings['exceptions']['restrict_seasons_to_num_days'] is used.
This could literally whitescreen "any day now" on an otherwise-working (and unsuspecting) site, so let's take the first step now and stop the error.
Remaining tasks
- Prevent the PHP errors and warnings with late checks.
- Investigate the root cause of the issue.
- Address the root cause of the issue.
Issue fork office_hours-3576684
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
mw4ll4c3 commentedComment #3
mw4ll4c3 commentedComment #5
mw4ll4c3 commentedComment #8
velmir_taky commentedMR !76 prevents the crash but doesn't fix why it happens — today's exception still won't show when horizon is 0.
Root cause:
keepExceptionDaysInHorizon()treats0as "remove all" andisInRange()has a wrong early return for$to==0. AlsoformatLabel()needs a null guard on$day.Fixed all three, added kernel test. Now horizon=0 means "today only" as expected.
2 CI failures are pre-existing (HookModuleUnitTest on 8.x-1.x), PHP 8.2 job fails due to missing docker image — both unrelated.
Comment #9
mw4ll4c3 commentedWell, "later" came a lot sooner than I figured! Thanks so much!
I've got some other stuff to tackle, but I'll get back in double-check your "real" fixes soon (if still needed by then).
Comment #10
johnvAfter fixing and committing #3576462: Formatter ignores "Date format for exception day" a few minutes ago, I cannot reproduce the error.
Indeed, it is not clearly defined how much 0 (or 1, 2) days in the future is. One might think '1 day in the future' means 'I can see tomorrow's exception.' So, 0 days means today.
Let me check your code proposal. Please check current dev version.
Comment #11
johnvIn OP, I guess you mean
$horizon = $settings['exceptions']['restrict_exceptions_to_num_days'];,not
$settings['exceptions']['restrict_seasons_to_num_days']?Comment #12
mw4ll4c3 commentedI did mean that. Sorry for any confusion.
Confirming: no PHP error with the latest commits to dev.
That's how the code is treating it -- except for
0.As of that latest commit:
0days in future: no exceptions show1days in future: today and tomorrow showWhile the whole thing could be changed from "days in future" to "days OF future" to keep
0hiding things... that has its own semantic problems, and to allow "just today" we'd have to change the meaning of>= 1as it exists.I think a reliable solution and plan would be:
0Comment #13
velmir_taky commentedThanks for confirming, @mw4ll4c3.
@johnv, the fix in #3576462 resolved the crash, but the underlying semantic issue remains: when
restrict_exceptions_to_num_daysis set to 0, today's exceptions still don't display.MR !77 addresses this by fixing three root causes:
1.
keepExceptionDaysInHorizon()— treated$horizon == 0as "remove all exceptions" instead of "show today only"2.
isInRange()— had an earlyreturn TRUEwhen$to == 0, bypassing proper date filtering3.
formatLabel()— added a null guard for $day to prevent the fatal error defensivelyThe MR also includes a kernel test covering horizon=0 (today only) and horizon=1 scenarios.
I agree with @mw4ll4c3's plan:
0should mean "today only." The MR implements exactly that. Could you review !77 when you get a chance?Comment #14
johnvI understand that 0 is an edge case, which is not handled consistently in the code.
I do not understand the use case. Please explain and share your formatter setings.
If you only want 'today' to be on the 'weeklyoffice hours' (I understand the both of you do not use 'seasons'), you could enable the setting:
"Replace weekday time slots with exception dates".
This will overwrite the values in the week formatter.
Comment #15
velmir_taky commented@johnv The use case: show only today's exception (e.g., "Closed today for renovation") without future ones cluttering the display. "0 days in future" naturally reads as "today only."
"Replace weekday time slots with exception dates" is a different thing — it controls how exceptions appear in relation to regular hours, not which exceptions are visible.
The two removed early returns were actually contradictory —
keepExceptionDaysInHorizontreated 0 as "remove all" whileisInRangetreated 0 as "keep all." After removing both,isInRange(0, 0)handles the range check correctly: today passes, tomorrow doesn't.Comment #16
mw4ll4c3 commentedThanks, velmir_taky. Nobody asks "why
37?" but it's a way better question. ;)My question isn't "why
0?" It's "why not0?" Two common answers:0means none0means allBoth ideas are supported by the code removed in MR77:
This wants to make
0show them all...2023-10-06: src/Plugin/Field/FieldType/OfficeHoursExceptionsItem.php:
...which never happens; because first this excludes them all...
2023-11-28: src/OfficeHoursItemListFormatter.php:
So, it (maybe) meant "all of them" for about two months, and now means "none."
"None" can be a toggle (or another formatter). "All" isn't supported at current but to cover it, I see two options:
EXCEPTION_HORIZON_MAXfrom999to99999(almost 300 years)The better option is "limit = blank = no limit = all" but that depends on what relies on it and where. The most I did on that was yank out
#requiredand get a PHP error trying to save a blank value, naturally.I'll be adjusting my MR:
EXCEPTION_HORIZON_MAXto99999I have this all built against yesterday's commit, but that breaks stuff, so I'll try to get something that works with 1.29 afterwards.
Comment #17
mw4ll4c3 commentedI opened #3577138: Allow exceptions to show for only today to handle all that from your latest commit.
The error's gone after that commit, so should we close this?
Comment #18
velmir_taky commentedTested the latest dev (commit 507eefc). The fatal error no longer occurs —
keepExceptionDaysInHorizon()filters out all exception days when horizon=0 before they reachformatLabel().The crash is resolved. The
0semantics question is tracked in #3577138.Thanks @johnv and @mw4ll4c3!
Comment #19
johnvI prefer the option "allow the input to be blank, rephrase the label to "limited to x days in the future"" above addint a checkbox, as currently proposed in #3577138: Allow exceptions to show for only today
I undestand the problem, but what is the problem with adding also 'tomorrow' to the formatter?
I guess as many people think: "I want to go tomorrow to location X. Let's check when they open."
as people check only on the day itself.
I guess '999' suffices, that is 3 years in advance.
Let me review the MR.
Sorry for not using MR myself.
Comment #21
johnvI made the option optional. The same functionality is added (opened) for seasons.
Please test and report back. Please re-open this issue if needed.
Comment #23
mw4ll4c3 commentedThank you!
Unlimited "days in future"
Blank/null is definitely the way to go for "unlimited," so thank you.
Max horizon at 999 days
Now that "unlimited" is allowed, 3000-year max does seem excessive ;)
But a 3-year max could be a little tight for some orgs / purposes. Maybe give it one more digit? It seems harmless enough.
I almost removed the limit entirely in my MR, because why have it at all? But it keeps the size of the
<input>reasonable, without getting into the CSS.The checkbox
I think there's a misunderstanding; sorry.
The checkbox prevents printing exceptions below the normal hours at all, ever, regardless of
restrict_exceptions_to_num_daysor anything else.That way, someone can enter all the exceptions they want, for use in the "Replace timeslots" feature, and never have it print anything below.
This is now needed because an admin can't do that with
0anymore.On the next release, there would need to be an update, setting the checkbox to
(restrict_exceptions_to_num_days !== 0), to keep things consistent for anyone who was using0to hide the exceptions.Comment #25
johnvad 1. Thanks, it was fun figuring this out.
ad 2. Indeed, max 999 or max 9999, who cares? Added with above commit.
However, I do not really see a use case why 2,5 year would not be enough. Please give me a use case.
ad 3. The 'Replace timeslots' option works independent from the 'horizon' option. An internal list of the upcoming 14 dates is built, including the changes by exceptions and seasons, and injected in the 'regular' weekday formatter. For obvious reasons, this cannot happen for 'seasonal' weekday formatter (Since this typically spans multiple weeks).
So, I do not see when an additional checkbox would be needed. Also here, please give me a use case. It all zouns rather hypothetical, now. (The same goes for the initial request: 'only today' - why would 'today and tomorrow' not be OK?
Kind regards,
Comment #26
mw4ll4c3 commentedI was happy to notice that, but that's a different thing. The checkbox I added controls whether anything shows below the regular hours.
Turning it off does what used to happen with
restrict_exceptions_to_num_days == 0. Suppose one of the 20k reported installs was doing that, because they could. Now they can't do it that way, and there's no other way to do it.I've already argued against having one thing be impossible for the sake of allowing another. when there's a way around that. That goes both ways, so I'll stick up for it again :)
a simple use case
Someone wants to show the regular weekdays, using "replace timeslots", but not print the exceptions below.
That's it. That's all. Maybe it would mess with their design. Maybe they just find it redundant.
So they turn off the "show exceptions below" checkbox.
barely more creative one
Think "teaser" and "full content" display modes.
One biz/org, with multiple locations.
- The locations each have own regular hours, which can be different.
- Each location can (will) have different "exceptions" -- closures and changes, planned or unplanned.
There's one page that lists the locations in brief. "Teaser" stuff, y'know: address, a little text, and the regular weekday hours. Only what the visitor need at a glance.
Click on one of the locations, go to its node. "Full content" stuff: staff listing, maybe more text, and... the regular weekday hours with a list of upcoming exceptions. Stuff that's helpful to a visitor that's asked for more info, by clicking into the one location.
One data source, two presentations. One in brief, one with extended info.
Same field, two placements, two sets of field settings... and one checkbox.
Comment #27
mw4ll4c3 commentedThe ultimate underlying reasoning is:
This is a module that fills a need, without a bunch of awkward wiring, has a nice client-friendly UI for input. It fills a gap.
With a few tweaks and options to fill its gaps, it can cover that need for about any conceivable case.
And yes, also a few inconceivable ones... maybe those
37people are out there somewhere... ;)Comment #28
johnv"Same field, two placements, two sets of field settings... and one checkbox."
I do not think tht is true. All relevant settings are in the 'formatter' setting, which is unique for each disply mode.
I think we have indeed created a problem.
As we now have a horizon of 9999 days, I propose to redefine the NULL option as 'never show'. For 'show all exceptions', one should add a fairly big horizon.
Comment #29
johnvComment #30
mw4ll4c3 commentedYou're right; sorry. I meant "two sets of that field's display settings" but you figured that out :)
Thank you for promising to work on it! Here are maybe my last thoughts on these two issues:
don't show exceptions below
I still think the right way is checkboxes for both "replace weekdays" and "show exceptions below"
The display settings offer two distinct, and optional, features: "printing exceptions within the weekdays" and "printing exceptions below the weekdays"
The checkbox means
[do that thing at all]. The other settings then mean[how to do that thing]when[do that thing at all].Clean and clear as it gets, for the user and the code: if that box isn't checked, everything else is ignored.
It's a common, flexible, and durable pattern for a reason: hard toggle for a feature, and then the feature's own settings. Frequently grouped, collapsible, hidden, disabled, etc. (I have of plenty of ideas about options for both features, but those are for other issues!)
A checkbox also skips any debate about the "limit to X days" setting...
allowing "no limit"
I think I saw a commit to make the max be 4-digit, so I agree with you: forget about it! :)
limit to X days
Now a solved problem, more or less. My only thought is allowing/using an empty number isn't great UX/UI.
We don't need to allow that, if we do one other thing...
final checkbox plea
Having no "limit" mean "no limit" was natural and intuitive.
Having no "limit" mean "don't do anything" would not be.
It would make its own sense... but only in the same way as if we made the exceptions date format optional, and no "exceptions date format" means "don't output the date" right? ;)
A toggle is simple, predictable, and independent of any decisions about the actual feature. Past, present, and future.
Please don't bring the limiter back into this, the poor thing! Not to give it a different edge case to control something else.
"Please don't use this one thing to control other things" covers like 95% of my concerns so far, even those other issues I'll post when I can :)
thanks again
I appreciate the work you've been putting in!
All of this is obviously just my opinion. Obviously I think I'm right, because it's my opinion XD
If you agree, there's code in #3577138: Allow exceptions to show for only today for the checkbox, which should still be good. The actual "don't print the exceptions below" is probably outdated, but it's the smallest part. (Obviously there's also
days = 0stuff too, but that was solved.)And thank you again!
Comment #32
johnvI guess you are right. I also get in trouble because:
- an empty field is not NULL, but '', resulting in ugly code.
- sometimes, isInRange() accepts NULL as an infinite horizon, which is intended.
Above commit makes the 'horizon' required again.
Another attempt: what about setting value '-1' as 'Do not show anything'?
Comment #33
mw4ll4c3 commentedThank you for staying right on top of this, and patiently hearing me out.
blank limit problems
Stuff like that is why I didn't bother when didn't "just work." More problems than it was worth, when we could just make
MAXabsurdly high.how to enable/disable the display
-1is better than blank, for the user and probably/maybe the code, but I'm still 100% "team checkbox"They each mean one thing and one thing only. Very clear to the user. Nice and clean in the code.
Checkbox sits there, does its job -- its only job -- and does it well. One FALSE stops the whole feature in its tracks.
number_of_days(or whatever it was) is always a number of days, with no exception. Even if it's negative (please don't ask!)It's harder to break, less likely to cause conflict, and lower-impact on things that are being reworked. Very relevant right now.
updates!
Don't forget, whichever you decide, because we've changed what
0means, so for existing display configs:limitto-1show_below(or whatever) toTRUEwherelimit > 0For new display configs, I think
FALSEshould be the default. "More stuff" should be opt-in by default... that just seems Drupal-y. That's probably how I had it.Comment #34
velmir_taky commentedWhile working on this issue, I noticed that the CI pipeline has multiple pre-existing test failures (44 errors + 3 failures) unrelated to this MR. Created a separate issue and MR to fix them: #3580345 / !79.
The MR for this issue (!77) is rebased on latest
8.x-1.xand ready for review. It includes:-
formatLabel()null guard for empty day values- Support for
-1horizon value as "do not show" (as suggested by @johnv in #32)- Updated form description and
#minfrom0to-1- 8 kernel tests covering horizon -1, 0, 1, 7 (default), past exceptions, no exceptions, and regular hours
Comment #35
johnv@root_emarketing , @velmir_taky, thanks for working on this issue, too.
I will take a closer look when we cocluded the discussion.
@mw4ll4c3 , some other options:
- Rename 'x days in the future' to just 'x days', where 0 = no days (none), 1=1 day (only today), etc.
Indeed, this might break some installations. For this, we may create an update hook with conditonal message, since that should be able to read the config.
- We also have the setting 'maintain exceptions' in the widget. If you do not want to see any exceptions, and you do not want to merge them into the current week (which will hide this week's exceptions), the you just should not maintain them! (Ofcourse the is onlh 1 widget for 2 display modes :-( )
Comment #37
mw4ll4c3 commentedYeah :(
A "workaround" for that (and all of this) would be two fields, but nobody wants to hand that over to the client/end-user.
I wouldn't say no to that, at least for the time being. It covers both needs (today and nothing) and the input-to-output makes sense.
I prefer that to using
-1, a lot, with all respect to the amount of working updating that MR must have taken.Long-term, leaving room for changes and additions is why I'm still #teamcheckbox.
My first (and so far best) idea involves three more options (across both features) and a change to this one... so I really like the idea of clean on/off switches that having nothing to do with any of that, y'know?
I don't even mind the idea of having the switch and making the other change to the label / behavior (because it makes that much sense).
Yeah, someone could turn it on but set it to show 0... but someone could also set up a View that only shows a node if its ID is two different numbers at the same time, so *shrug*
Comment #39
johnvI opted for no additional setting, for now.
0 now means: 'show no days'
1. now means 'Show only today'.
Patch adds a conversion for setting 'blank'.
Patch adds tests.
(I finally learned how to run tests locally, so they should pass.)
Still needs a change record.
Comment #40
johnvA change record is now also availalbe: https://www.drupal.org/node/3581807
Comment #42
johnvThe last 2 comments are edited. The change record is edited.
Comment #51
johnvComment #53
johnvThere was a problem with the horizon and strtotime(), with horizon = 30 days.
Comment #55
johnv