Satis is the lightweight, static repository generator for the composer package manager. It enables admins to provide PHP software packages inside airgapped CIs, OS packaging environments and restricted data centers.
Back in August I added a plugin mode to satis to make it work as a regular composer plugin. While working on it, I also fixed some preexisting issues found by static analysis and made it compatible with the recent composer versions 2.3 and 2.4.
This week, the upstream maintainers merged my contribution. I feel a bit satis-fied π
Why make it a plugin?
When looking under the hood, it is easy to see that satis always has been some awkward kind of plugin or extension. It literally sits on top of an internal copy of composer. It hooks into its internals in not quite transparent ways, it uses its class and interface organs for its own vital functions. You might call it a parasite that attaches to composer’s body for its own needs. There are downsides to this approach. The first is that you need a private copy of composer. The second is that any refactoring of composer internals likely breaks satis compatibility. That happened some time ago when composer 2.3 and 2.4 were released and not for the first time. Composer has a maturing plugin API with nice, well-defined integration points. It provides some means to overload or amend core functionality but it also provides messaging between core and plugins. I only did the bare minimum work to make satis hook into the plugin API and not break the standalone mode. When installed as a dependency, package resolution will ensure that the API versions used by satis matches the API versions provided by the underlying composer.
I don’t quite understand… What is the benefit?
By itself, this change provides little benefit. It is a feature enabling feature.
Satis can be further refactored to make compatibility bread less often
Satis can send and receive events from composer or other composer plugins. This enables running satis as part off a custom command. Think passing unit and integration tests of a project and then conditionally updating the staging or canary package repository.
Satis’ schema could be amended to make a project’s root package also function as an instruction to build a repository of all dependencies with almost zero configuration. Add this to a workflow or add a collaborator plugin that handles the necessary push/release and you have a powerful tool for CI and developer laptop alike.
But as I went along, I also re-aligned satis with the latest breaking changes inside composer 2.3/2.4. This will benefit users who do not care about the whole plugin story.
What’s next?
With satis 3.0-dev merging this initial change, the next steps are obvious, but not urgent. Making the new plugin mode play nice with the latest composer was already easier than fixing the standalone mode. Satis still has an internal, now updated dependency copy of composer which is only run in standalone mode.
Standalone mode should be refactored to be just a thin wrapper around composer calling into its satis plugin. Keeping intrusion into composer internals to the bare minimum to hide the builtin commands and re-brand it as satis, this would make breakage on upcoming updates much less likely. Eventually, we can maybe stop carrying around a source code copy of composer at all.
Finally, there is reaping the benefits. I want to leverage composer/satis functionality inside the horde/components tool. Rolling out new versions of horde stuff could be so much easier.
Regular readers of this blog and many other are aware that PHP 7.4 will stop receiving security updates when PHP 8.2 comes out in November. This has made many horde admins question if they can continue to run Horde. Some events in life have made progress slower than originally planned. So where are we?
Confirmed running under PHP 8.1 and composer 2.4
horde/base in Browser
essential Horde Base CLI tools like horde-db-migrate and hordectl
horde/base portal blocks and admin area
horde/components developer tool
horde/turba Addressbook App Reading and writing contacts in the UI
horde/mnemo Notes App UI and webdav
horde/nag Tasks Apps UI, webdav, caldav
horde/kronolith Calendar App UI, webdav, CalDAV
horde/passwd Password App – Changing passwords worked with the hordeauth driver
horde/gollem File Manager App – very limited testing so far
horde/imp Webmail – very limited testing.
I run on a setup with openssl3 and a recent mariadb against dovecot and postfix. You can also consume the openSUSE 15.4 based containers built nightly. There is still considerable log spam from deprecation notices: Mostly tentative return types and signatures, also some use of deprecated functionality like strftime. Each night a few of these disappear. They don’t stop you from running horde apps.
I also have an eye on PHP 8.2 compatibility – So far, there should not be too many surprises. I also check most unit tests against the development version of PHPUnit 10.
This code is quite solid on PHP 7.4 – production users run on it. On PHP 8.1 I consider it ready for adoption tests. Breakage is expected, feedback is welcome. Be sure to have a backup of the database and of any mail accounts you connect to it. There is a lot to be done over the next few weeks.
If it does not run for your combination of drivers, please contact me via the horde mailing list.
Known caveats:
imp config SHOULD have an explicit cache setting: Set it to false to disable caching or to ‘cache’ to use Horde’s default cache. The ‘sql’ option also seems to work but I do not recommend it.
The RPC interface has seen very little testing. The json-rpc protocol should work. I have no desire to look into xmlrpc though unless somebody voices his needs. Beware, the xmlrpc extension has moved out of mainstream into pecl.
I do not have the necessary setup to comment on ActiveSync currently
Kolab integration is very likely broken. I don’t think anybody really uses recent horde with ancient kolab versions.
Most likely the SyncMl and PHPGroupware drivers are useless. If anybody really uses that bridge, please give feedback
I usually test against sabre/dav 4.4 – if you use anything else and see bugs, let me know
I don’t currently test against postgresql. MariaDB, MySQL, PerconaDB should work.
As PHP’s LDAP extension has moved from resources to objects, the LDAP authentication and addressbook drivers likely need an update. I do not currently test against LDAP but this is something I want to change
I know my former colleagues run LDAP and Redis so likely they will give some feedback in that area – Cannot comment on the timeline. I will offer a redis option for the maintaina container setup soonish.
If you ever read a github pull request or similar extension proposal, you will likely have seen side by side comparisons of the original and the changed file. You may also have seen some text format that highlights only differences and a little context but hides the unchanged rest of the file. Both of these formats are called Diff, named after the popular diff and patch utilities dating back to ancient Unix times. The git diff command does something very similar. The horde/text_diff library and its ancestor, the pear/text_diff library, are tools to generate and format such difference information for different usage scenarios.
Apart from Horde’s internal usage in its repository viewer, horde/chora, and its wiki software, horde/wicked, the tool is also used by external parties. WebSVN maintainer Michael O. approached me because he wanted to use a PHP 8.2 ready version of horde/text_diff to substitute an older component which did not do the job. Michael has been very helpful in getting me started, pointing me to some issues to solve and also providing his own solutions in some parts. The result is a conservative update of horde/text_diff that will run in the upcoming versions of PHP without causing any trouble. But this is only where I started.
Breaking bad habits
A closer look at the internal structure of the library showed that it deserved a major overhaul. The solution was to refrain from a verbatim upgrade to namespaces and the likes but to actually change some things. This meant breaking backwards compatibility. I go to great lengths to keep a conservative drop-in version of everything I touch in the lib/ folder. Sometimes it is just an interface or wrapper, sometimes the new and old code do not really share a lot.
I began with adding type hints to most methods. Targetting PHP 8.1+ for the src/ path allows to use union types and intersection types. A lot of knowledge hidden in phpdoc comments is moved into actual code and makes it more robust.
Exploring the code for base classes and interfaces, I noticed that some things I did not like.
Method signatures did not add up
Some method signatures did not add up. Depending on the type of Diff Engine, the diff() method would take different types of arguments. The interface was mixing the specifics of how the diff engine is set up with the command to create a list of operations objects. Loading the engine is now separated from running it. The running method is now always called in the same way.
Internal dependency creation
The Diff, Threeway Diff and Mapped Diff utilities all created their diff engine internally. To do this, they needed a very flexible constructor that allowed passing whatever is needed to set up the actual engine. That was bad enough but they also did it in different ways. The Differs’ constructor now only accepted a pre-constructed engine. For convenience, I created a factory which would take over the responsibilities originally assigned to the differs’ constructor: Building a differ from input and if no specific differ was chosen, selecting one by some priority logic. In the end it turned out that the Differ does not need the engine at all but rather needs the product of the Engine: A set of operations to transform document A to document B. Born is the OperationList class. I did not want to just pass an array. I added a small static method as a named constructor. It frees the actual constructor of too many responsibilities and allows to keep the interface clean and strict.
More explicit type juggling
Creation if diffs contains some interesting math. The algorithms use a lot of short variable names and operations that make sense if you know the underlying theory but otherwise look like garbage. I added some explicit conversions between string and integer and made some changes to ensure a number zero or an empty string is not mistaken for a “false” or “null” value which would have another meaning. Overall it is now much easier for static analyzers to spot any issues.
Dual stacks have a price tag
Essentially maintaining two different sets of the library comes with some cost. One must ensure that unit tests targeted at the newer platform are not run when testing for compatibility with the older platform. The conservative lib/ is ready to run PHP 7.4 code but the modern version in src/ must be transpiled to be run in PHP 7.4 – I do that on release but still it is another aspect that needs minding. As I also use automated tools for some upgrade tasks, I need to ensure I do not upgrade the lib/ path. The price is worth it as I cannot convert the code base at once and I want to provide a good development experience to all who are caught in between maintaining an older release or creating new code. I am in that spot myself. Essentially it allows me to run two conflicting major versions of some critical libraries and pick the right one for different sub systems. The need will go away as code is gradually migrating towards the newer implementations. At some point a next major version will drop the conservative path. Anybody interested is free to maintain the older major version and keep using it.
Upcoming work
I consider the external interface of the newer horde/text_diff implementation fairly stable by now. Internally, however, there is a lot of room for improvement. Some functionality should move out of the base classes and into separate traits – which the base classes will use. Some getters should be added and used, preparing to move some public variables to internal state in a next major release. The new OperationList gets unloaded to plain arrays in several places – It needs to learn some tricks without degrading into a glorified array. None of this should stop early adopters from using the new code base. None of this is supposed to break any user code.
Out of scope for now
There are some items which I decided to postpone for now. One thing which bothers me is the amount of dependencies. While a dependency on horde/exception makes sense, it pulls in horde/translation for no good reason. Horde\Util is pulled in but really only used in two places: A horde/string call which could be reduced to a direct call to an internal function and one call to a helper for handling temporary files. That helper should maybe live in its own library, nicely decoupled from unrelated utilities. There was a reason why they were packed together but it is no longer relevant.
Also, some functionality is missing in the Xdiff-based engine. Most distributions do not even offer php-xdiff, including my own development platform. I will add that feature once I get it into CI and into the development setup. I do not want to delay other items to do that right now. Patches welcome π
Behind every lofty architecture mantra there is mundane execution. This is best left to tools and I don’t mean anybody in particular but programs that help us make better programs. It basically goes like this: Build tool. Use tool. Build better tool. Build tool to build better tool. Build better tool to build better tool faster. And so on. Implementing this in practice can be quite boring but the alternative is to do boring things again and again and again and that’s enough already. So let’s see.
Maintaining 100+ libraries and programs involves doing a few things over and over again. Automating these seems natural but requires some thought. Developers want to spend their time in interesting and useful ways. Querying and manipulating git repositories is repetitive. Updating a changelog file with a select subset of messages also present in the git commits is repetitive. Rewriting project metadata and updating CI jobs for new PHPUnit and PHP versions or base operating systems is repetitive and requires no brains at all, why should I do this 100+ times?
Off the shelf tools
Using tools that already exist and are maintained by other parties is a no-brainer. Which tools can help?
PHPUnit helps us spot and eliminate regressions before any user is affected. The tool itself is maintained by Sebastian Bergmann but writing and upgrading the actual test code is a chore.
PHPStan or Psalm – I prefer PHPStan – are static analyzers which help developers spot places where signatures, types and assumptions don’t add up. To get the best out of it, either phpdoc annotations or parameter and return types must be added. No tests to write, which is good – but PHPStan is organized in progressively strict levels and each library needs to be checked against the level it is supposed to pass. Micromanaging that is boring as hell, tools are needed.
php-cs-fixer is developed by friendsofphp – it is a basic code manipulation tool which helps anywhere from adhering to PSR-12 or PER-1 to automatically upgrading from array() to [] notation. Configuring this beast is easy but ensuring the most current rules are used in every project is another burden.
rector is another tool that transpiles code either up or down to select standards. It will move implicit knowledge or phpdoc data into actual code or do the different thing. It will choose older ways to express something over new ones or vice versa. Configuring it to do only what is helpful is quite a challenge. Also ensuring the most recent config is used is just boring and cumbersome. Tool needed.
Homegrown tools
The horde project has some home grown tools that can help but need development themselves.
horde/git-tools by Michael Rubinsky used to be the way to assemble a bleeding edge developer copy from zillions of github repos. In a modern composer based installation this tool is less useful but it contains a lot of interesting capabilities that should be factored out
horde/components can generate composer and pear metadata from a self-defined yaml format. It can create tar archives from repositories, implements a basic workflow engine for release and quality check tasks and does some other things. Its internal architecture is rooted in history and while some of its functionality seems out of touch with 2022, many other parts deserve expanding or factoring out into modern self-contained libraries for reuse.
horde/hordectl is a command line tool to interact with a Horde installation. Inject users and passwords, configure permissions, groups or app-specific resources from yaml files and defaults. It needs some upgrading, it could do so much more to facilitate proof of concept, showcase or CI installations.
horde/horde-installer-plugin is a plugin for composer that helps bootstrap a horde installation and its web-readable part. Much of its code would best be moved out to separate libraries.
Building blocks
Existing and new libraries should inherit functionality moved out from existing tools or newly created
horde/vcs is a version control library. Its main origin are the horde/chora application and the installation/development tools. Recently I began to move or re-implement code from git-tools and horde/components into this library. I am less interested in the rcs, cvs and svn parts. The original library followed an approach abstracting the differences between git, cvs, svn & friends. This limits its usefulness. I see how it facilitates creating an application that consumes and shows code from these. Still, there should be a lower level of abstraction that provides the unique capabilities of git in a programmatic fashion. This is one thing I currently work on
horde/rampage used to be a dead end but I am reusing the library for deployment and introspection related code factored out from other tools.
horde/filesystem is a new library, focused on object-oriented filesystem traversal and manipulation. Still very immature but I hope to turn it into a standalone and reusable tool.
horde/registry is the stub of an upcoming redesign of the core bootstrapping process. No more globals, reliance on PSR-11 DI containers and PSR-4 autoloading – this registry will do less than its ancestors yet be much more powerful and easy to use. This is still much work.
horde/cli_modular is a tool to write extensible, pluggable commandline interfaces. It is used by horde/git-tools, horde/components, horde/hordectl and a few others. In the current upgrade cycle some redesign is necessary to make it viable for modern environments and free it from problems already solved by autoloaders or DI containers.
So much work to do but devoting some time to better tools is better than doing mindless conversions of existing code over and over.
Developers sometimes choose not to use the latest available language features that would be appropriate to tackle a problem for fear of alienating users and collaborators. This is a bad habit and we should stop doing that. Part of the solution are transpilers. What are transpilers, where are they used and what is the benefit? Why should we consider transpiling all our code?
I cut this piece from an upcoming article that is way too long anyway. I made this new article by reusing and reshaping existing text for a new audience and frame. You are reading a new text that first was built as a part of another text. – Yes. This was transpiling: Rephrasing an input, including externally supplied, derivative or implicit facts about it to an output that generally expresses the same. Excuse me, what? Let me go into some details.
Transpiling: Saying the same but different
In software, transpilers are also known as source to source compilers. They take in a program written in one language and write out a roughly equivalent program for another language. The other language may be another version or dialect of the input language or something entirely different. Don’t be too critical about the words: transpilers are just like all other compilers. Source code is machine-intelligible, otherwise it could not be compiled. Machine code is intelligible by humans, at least in principle.
Preprocessors are transpilers
A preprocessor is essentially a transpiler even if it does not interpret the program itself. The C language preprocessor is a mighty tool. It allows you to write placeholders that will be exchanged for code before the actual C compiler touches it. These placeholders may even have parameters or make the program include more code only if needed. Concatenating many source files into one and minifying these by stripping unnecessary whitespace can also be seen as a primitive form of transpiling.
Coding Style Fixers are transpilers
Automatic tools that edit your source code are transpilers. They might only exchange tabs for four space characters or make sure your curly braces are always in the same place or they may do much more involved stuff. For example php-cs-fixer transforms your technically correct code written in plain PHP into technically correct code in standards-conforming plain PHP. One such standard is PSR-2, later deprecated in favor of PSR-12 and PER-1 – these are all maintained by the PHP FIG. Software projects may define their own standards and configure tools to transpile existing code to conform to their evolving standards.
Compilers are transpilers
A compiler is a transpiler. It takes in the source code and builds a machine-executable artifact, the binary code. It might also build a byte code for some execution platform like Java’s JVM. It might build code for a relatively primitive intermediate language like CIL or a machine specific Assembly Language. Another compiler or an interpreter will be able to work with that to either run the software or turn it into a further optimized format. These transformations are potentially lossy.
Decompilers are transpilers
Earlier in life I used tools like SoftICE that would translate back from binary machine instructions to Assembly Language so that I could understand what exactly the machine is doing and make it do some unorthodox things. Compiling back from Machine Code to the machine-specific Assembly Language is technically possible and lossless but the result is not pretty.
Lost in translation
When humans rewrite a text for another target audience, they will remove remarks that are unintelligible or irrelevant to the new audience. They may also add things that were previously understood without saying or generally known in the former audience. Transpilers do the same. When they transpile for machine consumption, they remove what the machine has no interest in: Whitespace, comments, etc. They can also replace higher concept expressions by detailed instructions in lower concepts. Imagine I compiled a program from assembly language into binary machine code and then decompiled back to assembly language. Is it still the same program? Yes and no. It is still able to compile back into the same machine program. It does not look like the program I originally wrote. Any macro with a meaninful name was replaced by the equivalent step by step instructions without any guidance what their intention is. Any comments I wrote to help me or others navigate and reason about the code are lost in translation. The same is true anytime when we translate from a more expressive higher concept to a lower concept. Any implicit defaults I did not express now show up as deliberate and explicit artifacts or the other way around, depending on tool settings.
Lost in Translation but with Humans
You may know that from machine translated text. Put any non trivial text into a machine translator, translate it from English to Russian to Chinese to German and then back to English. In the best case, it still expresses the core concept. In the worst case it is complete garbage and misleading.
Another such thing are Controlled Languages like Simple English, FranΓ§ais fondamental, Leichte Sprache, etc. They use a reduced syntax with less options and variations and a smaller selection of words. Some like Aviation English or Seaspeak also try to reduce chance for fatal ambiguity or mishearing.
These reduced languages are supposedly helpful for those who cannot read very well, are still learning the language or have a learning disability. They may also enable speakers of a closely related foreign language to understand a text and they generally cater to machine translation. For those who easily navigate the full blown syntax and vocabulary and can cope with ambiguity and pun, simplified language can be repetitive, boring and an unnecessary burden to actual communication. Choosing a phrase or well known roughly fitting word over a less used but more precise word is an intellectual effort. Reading a known specific word can be easier on the brain than constructing a meaning from a group of more common words. Speaking to an expert in a language deliberately evading technical terms may have an unintended subtext. Speaking to a layman in lawyerese or technobabble might not only make it hard for them to understand you but also hard for them to like you. Readers will leave if I make this section any longer.
Useful application
Now that everybody is bored enough, let’s see why it is useful and how good it is.
Upgrading Code to newer language versions
You can use a transpiler to upgrade code to a newer version of the language. Why would you want that? Languages evolve. New features are added that allow you to write less and mean the same. Old expressions become ambiguous by new syntax and features. Keywords can be reserved that previously weren’t. Old features become deprecated and will finally stop working in later versions. A transpiler can rewrite your code in a way that it will run in the current and next version of a language. It can also move meta information from comments or annotations into actual language code.
// Before
/**
* @readonly
* @access private
* @var FluffProviderInterface Tool that adds bovine output
*/
var $fluffProvider;
/**
* Constructs an example
*
* @access public
*
* @param IllustrableTopicInterface A topic to explain by example
* @param bool $padWithFluff Whether to make it longer than needed
* @param int $targetLength How long to make the article
*
* @return string The Article
*/
function constructExample($topic, $padWithFluff=true, $targetLength=3000)
Now that’s what I call a contrived example. Code might look like this if it was originally written in PHP 4 and later enhanced over the years, only using new expressiveness where needed. While it technically runs, it is not how we would possibly write it today.
// After
/**
* @var FluffProviderInterface Tool that adds bovine output
*/
private readonly FluffProviderInterface $fluffProvider;
/**
* Constructs an example
*
* @param IllustrableTopicInterface A topic to explain by example
* @param bool $padWithFluff Whether to make it longer than needed
* @param int $targetLength How long to make the article
*
* @return string The Article
*/
public function constructExample(
IllustrableTopicInterface $topic,
bool $padWithFluff=true,
int $targetLength=3000
): string
That could be the output of a transpiler. It takes meta information from controlled language in the comments and uses the advanced grammar of the improved PHP language to express them. In other words, the upgraded code has turned instructions for the human or for external tools into instructions that the language can actually enforce at runtime. Before it helped you understand what to put in and what to expect out. Now it forbids you from putting in the wrong things and errors if the code tries to give back anything but text.
It may drop comments that are already expressed in the actual code. Some project standards suggest to drop @param and @return altogether to make the code more consise to read. I am a little conservative on this topic. A documentation block may be removed if it does not contain any guidance beyond the code. There is no need to rephrase “this is the constructor” or “The parameter of type integer with name $targetLength tells you how long it should be”. But sometimes things deserve explaining and sometimes the type annotations exceed what actual the language expresses. Intersection types are PHP 8.1+. PHP 8.2 can express “return this class or false but not true” while before the language only allowed “This class or a boolean (either true or false)”. Annotations can be read by tools to work with your code. As demonstrated, a transpiler can use them to rewrite your code to a more robust form. Static analyzers can detect type mismatch that can lead to all sorts of bugs and misbehaviours. Documentation generators can strip away the actual code and transform the comments and structural information into something you can easily navigate and reason about. Code including high concept and documentation is first and foremost for humans. Adapting it for machines often means dumbing it down.
Downgrading to an older platform or language version
Code can be transformed in the other direction, stripping or replacing advanced expressiveness to make the older runtime understand the code. This is very popular with Frontend Developers: Both Javascript and CSS are usually no longer shipped the way they are written. A variety of type safe and advanced languages exist that are not even intended to be run in their source form but compiled down to a more or less modern standard of JavaScript, then minified to the smallest valid representation. Possibly variable and function identifiers are changed to avoid them colliding between unrelated software loaded into the same browser. In other languages, we are used to develop against a target baseline and only use the features it provides, plus annotations for concepts it does not support. We choose the baseline by deciding on the lowest platform we want to or have to support. This is jolly insane and I mean it in a nice way.
Imagine we create a book for small children. We will first create a compelling story, lovely characters and possibly some educational tangent using our words and our thoughts, the level of abstraction we are fluent in and the tools we can handle. We finally take care to adapt wording, level of detail and difficult concepts to fit the desired product. We don’t write to the agent, the publisher or the printing house in baby english. So why should we use anything less than our own development environment supports? It is not healthy. Outside very special situations or for the joy of it, we generally don’t work with one hand tied to the back, using antiquated tools and following outmoded standards.
This Catapult resembles the state of the art centuries ago. Shooting it is fun. For any other purpose it is the wrong tool.
If we cater to the lowest assumable set of capabilities at development time, we limit ourselves in a costly way. We cannot benefit from the latest and most convenient, i.e. effortless and reliable set of tools. We are slower than we could be, we will make more mistakes and it will exhaust us more than needed.
Provided our production pipeline from the development laptop or container to the CI are able to work with the latest tools, we can use them.
Deliver using a transpiler
The source branch should always target your development baseline, tools as modern as you can come by. Delivery artifacts, i.e. released versions, should deviate from the source distribution anyway:
Why should you ship build time dependencies with a release?
Why should you ship CI recipes or linter configurations with a release?
Depending on circumstances, shipping the unit tests might be useful or waste.
You would not normally ship your .git directory, would you?
Adding a transpiler step is just another item, just another reason. Transpiling to your lowest supported baseline is not really different from zipping a file, editing a version string or running a test suite to abort faulty builds before they ship. But still, it is not perfect. The shipped code will run on the oldest supported environment but it will miss many runtime benefits of newer versions. This is especially true if your library is a build time dependency of another project. In the best scenario, a build for a fairly recent but reasonable platform expectation exists and another build for an well-chosen older target exists. Both need to run through the test suite and ideally the older build will pass the test suite both when actually run on the old platform and when run on an upgraded platform. There are some details, edge cases and precautions needed to make this feasible and reliable. This will be detailed in an upcoming article which just shrank by a good portion.
Tonight’s work was polishing maintaina’s version of horde/yaml. The test suite and the CI jobs now run successfully both on PHP 7.4 and PHP 8.1. The usual bits of upgrading, you might say.
However, I had my eye on horde/yaml for a reason. I wanted to use it as part of my improvements to the horde composer plugin. Composer famously has been rejecting reading any yaml files for roughly a decade so I need to roll my own yaml reader if I want to deal with horde’s changelog, project definition and a few other files. I wanted to keep the footprint small though, not install half a framework along with the installer utility.
You never walk alone – There is no singular in horde
The library used to pull in quite a zoo of horde friends and I wondered why exactly. The answer was quite surprising. There is no singular in horde. Almost none of the packages can be installed without at least one dependency. In detail, horde/yaml pulled in horde/util because it used exactly one static method in exactly one place. It turned out while that method is powerful and has many use cases, it was used in a way that resulted in a very simple call to a PHP builtin function. I decided whenever the library is not around I will directly call that function and lose whatever benefits the other library might grant over this. This pattern is called graceful degradation. If a feature is missing, deliver the next best available alternative rather than just give up and fail. The util library kept installing although the yaml parser no longer needed it. The parser still depended on the horde/exception package which in turn depended on horde/translation and a few other helpers. Finally horde/test also depended on horde/util. It was time to allow a way out. While all of these are installed in any horde centric use case, anybody who wants only a neat little yaml parser would be very unhappy about that dependency crowd.
Alternative exceptions
The library already used native PHP exceptions in many places but wrapped Horde exceptions for some more intricate cases. While this is all desirable, we can also do without it. If the horde/exception package is available, it will be used. Otherwise one of the builtin exceptions is raised instead. This required to update the test suite to make it run correctly either way. But what is the point if the test suite will install horde/util anyway?
Running tests without horde/test unless it is available
I noticed none of the tests really depended on horde/test functionality. Only some glue code for utilities like the horde/test runner or horde/components really did anything useful. I decided to change the bootstrap code so that it would not outright fail if horde/test was not around. Now the library can be tested by an external phpunit installation, phar or whatever. It does not even need a “composer install” run, only a “composer dump-autoload --dev” to build the autoloader file.
A standalone yaml parser
The final result is a horde/yaml that still provides all integrations when run together with its peer libraries but can be used as a standalone yaml parser if that is desirable. I hope this helps make the package more popular outside the horde context.
Lessons learned
Sometimes less is more. Factoring out aspects for reuse is good. Factoring out aspects into all-powerful utility libraries like “util”, “support” and the likes can glue an otherwise self contained piece of software together with too many other things. That makes them less attractive and harder to work with. Gracefully running nevertheless is one part. The other is redesigning said packages which cover too many aspects at once. This is a topic for another article in another night though.
PHP 8.1 has introduced tentative return types. This can make older code spit out warnings like mad. Let’s examine what it means and how to deal with it.
PHP 8.1 Warnings that will become syntax errors by PHP 9
PHP 7.4 to PHP 8.1 have introduced a lot of parameter types and return types to builtin classes that previously did not have types in their signatures. This would make any class extending builtin classes or implementing builtin interface break for the new PHP versions if they did not have the return type specified and would create interesting breaks on older PHP versions.
Remember the Liskov Substitution Principle (LSP): Objects of a parent class can be replaced by objects of the child class. For this to work, several conditions must be met:
Return types must be covariant, meaning the same as the parent’s return type or a more specific sub type. If the parent class guarantees to return an iterable then the child class must guarantee an iterable or something more specific, i.e. an ArrayObject or a MyFooList (implements an iterable type).
Parameter types must be contravariant, meaning they must allow all parameters the parent would allow, and can possibly allow a wider set of inputs. The child class cannot un-allow anything the parent would accept.
Exceptions are often forgotten: Barbara Liskov‘s work implies that Exceptions thrown by a subtype must be the same type as exceptions of the parent type. This allows for child exceptions or wrapping unrelated exceptions into related types.
There are some more expectations on the behaviour and semantics of derived classes which usually are ignored by many novice and intermediate programmers and sadly also some senior architects.
Historically, PHP was very lax about any of these requirements. PHP 4 brought classes and some limited inheritance, PHP 5 brought private and protected methods and properties, a new type of constructor and some very limited type system for arrays and classes. PHP 7 and 8 brought union types, intersection types, return type declaration and primitive types (int, string) along with the strict mode. Each version introduced some more constraints on inheritance in the spirit of LSP and gave us the traits feature to keep us from abusing inheritance for language assisted copy/paste. Each version also came with some subtle exceptions from LSP rules to allow backward compatibility, at least for the time being.
In parallel to return types, a lot of internal classes have changed from returning bare PHP resources to actual classes. Library code usually hides these differences and can be upgraded to work with either, depending on which PHP version they run. However, libraries that extend internal classes rather than wrapping them are facing some issues.
PHP’s solution was to make the return type tentative. Extending classes are supposed to declare compatible return types. Incompatible return types are a syntax error just like in a normal user class. Missing return types, no declaration at all, however, are handled more gracefully. Before PHP 8.1, they were silently ignored. Starting in PHP 8.1 they still work as before, but emit a deprecation notice to PHP’s error output, usually a logfile or the systemd journal. Starting in PHP 9 they will be turned into regular syntax errors.
Why is this good?
Adding types to internal classes helps developers use return values more correctly. Modern editors and IDEs like Visual Studio Code or PhpStorm are aware of class signatures and can inform the users about the intended types just as they write the code. Static analysis tools recognize types and signatures as well as some special comments (phpdoc) and can give insight into more subtle edge cases. One such utility is PHPStan. All together they allow us to be more productive, write more robust code with less bugs of the trivial and not so trivial types. This frees us from being super smart on the technical level or hunting down inexplicable, hard to reproduce issues. We can use this saved time and effort to be smarter on the conceptual level: This is where features grow, this is where most performance is usually won and lost.
Why is this bad?
Change is inevitable. Change is usually for the better, even if we don’t see it at first. However, change brings maintenance burden. In the past, Linux distributions often shipped well-tested but old PHP versions to begin with and release cycles, especially in the enterprise environment, were quite long. Developers would have had to write code that would run on the most recent PHP as well as versions released many years ago. Administrators would frown upon developers who always wanted the latest, greatest versions for their silly PHP toys. Real men use Perl anyway. But this has changed a lot. Developers and administrators now coexist peacefully in DevOps teams, CI pipelines bundle OS components, PHP and the latest application code into container images. Containers are bundled into deployments and somebody out there on the internet consumes these bundles with a shell oneliner or a click in some UI and expects a whole zoo of software to start up and cooperate. Things are moving much faster now. The larger the code base you own, the more time you spend on technically boring conversion work. You can be lucky and leverage a lot of external code. The downside is you are now caught in the intersection between PHP’s release cycle and the external code developer’s release cycles – the more vendors the more components that must be kept in sync. PHP 9 is far away but the time window for these technical changes can be more narrow than you think. After all, you have to deliver features and keep up with subtle changes in the behaviour and API of databases, consumed external services, key/value stores and so on. Just keeping a larger piece of software running in a changing and diverse environment is actually hard work. Let’s look at the available options.
How to silence it – Without breaking PHP 5
You can leverage a new attribute introduced in PHP 8.1 – just add it to your code base right above the method. It signals to PHP that it should not emit a notice about the mismatch.
<?php
class Horde_Ancient_ArrayType implements ArrayAccess {
/**
* @return bool PHP 8.1 would require a bool return time
*/
#[\ReturnTypeWillChange]
public function offsetExists(mixed $offset) {
// Implementation here
}
...
}
Older PHP that does not know this attribute would just read it as a comment. Hash style comments have been around for long and while most style guides avoid them, they are enabled in all modern PHP versions. This approach will work fine until PHP 9.
How to fix it properly – Be safe for upcoming PHP 9
The obvious way forward is to just change the signature of your extending class.
<?php
class Horde_Ancient_ArrayType implements ArrayAccess {
public function offsetExists(mixed $offset): bool {
// Implementation here
}
...
}
The change itself is simple enough. If your class is part of a wider type hierarchy, you will need to update all downstream inheriting classes as well. If you like to, you can also reduce checking code on the receiving side that previously guarded against unexpected input or just satisfied your static analyzer. Tools like rector can help you mastering such tedious upgrade work over a large code base though they require non-trivial time to properly configure them for your specific needs. There are experts out there who can do this for you if you like to hire professional services – but don’t ask me please.
<?php
...
$exists = isset($ancient['element1']);
// No longer necessary - never mind the silly example
if (!is_bool($exists)) {
throw new Horde_Exception("Some issue or other");
}
Doing nothing is OK – For now
In many situations, reacting at all is a choice and not doing anything is a sane alternative. As always, it depends. You are planning a major refactoring, replace larger parts of code with a new library or major revision? Your customer has signaled he might move away from the code base? Don’t invest.
My approach for the maintaina-com code base
The maintaina-com github organization holds a fork of the Horde groupware and framework. With over 100 libraries and applications to maintain, it is a good example. While end users likely won’t see the difference, the code base is adapted for modern PHP versions, more recent major versions of external libraries, databases, composer as an installer and autoloader. Newer bits of code support the PHP-FIG standards from PSR-3 Logging to PSR-18 HTTP Client. Older pieces show their age in design and implementation. Exactly the amount of change described above makes it hard to merge back changes into the official horde builds – this is an ongoing effort. Changes from upstream horde are integrated as soon as possible.
I approach signature upgrades and other such tasks by grouping code in three categories:
Traditional code lives in /lib and follows a coding convention largely founded on PHP 5.x idioms, PSR-0 autoloading, PSR-1/PSR-2 guidelines with some exceptions. This code is mostly unnamespaced, some of it traces back into PHP 4 times. Coverage with unit tests is mostly good for libraries and lacking for applications. Some of this is just wrapping more modern implementations for consumption by older code, hiding incompatible improvements. This is where I adopt attributes when upstream does or when I happen to touch code but I make no active effort.
More modern code in /src follows PSR-4 autoloading, namespaces, PSR-12 coding standards, modern signatures and features to an increasing degree. This generally MUST run on PHP 7.4 and SHOULD run on recent PHP releases. This is where I actively pursue forward compatibility. Unit tests usually get a facelift to these standards and PHPStan coverage in a systematic fashion.
Glue code, utility code and interfaces are touched in a pragmatic fashion. Major rewrites come with updated standards and approaches, minor updates mostly ensure compatibility with the ever changing ecosystem.
If you maintain a large code base, you are likely know your own tradeoffs, the efforts you keep postponing in favour of more interesting or more urgent work until you have to. Your strategy might be different, porting everything to a certain baseline standard before approaching the next angle maybe. There is no right or wrong as long as it works for you.
The maintaina-com/horde-installer-plugin has seen a few changes lately. This piece is run on every composer install or update in a horde installation. A bug in it can easily break everything from CI pipelines to new horde installations and it is quite time consuming to debug. I usually try to limit changes.
Two codebases merged
In the 2.3.0 release of November 2021 I added a new custom command horde-reconfigure which does all the background magic of looking up or creating config snippets and linking them to the appropriate places, linking javascript from addon packages to web-readable locations and so on. This is essentially the same as the installer plugin does but on demand. A user can run this when he has added new config files to an existing installation. Unfortunately the runtime environment of the installer plugin and the custom command are very different in terms of available IO, known paths and details about the package. I took the opportunity to clean up code, refactor and rethink some parts to do the same things but in a more comprehensible way. As I was aware of the risks I decided to leave the original installer untouched. I got some feedback and used it myself. It seemed to work well enough.
For the 2.4.0 release I decided to finally rebase the installer onto the command codebase and get rid of the older code. It turned out that the reconfigure command was lacking some details which are important in the install use case. Nobody ever complained because these settings are usually not changed/deleted outside install/update phase. As of v2.4.4 the installer is feature complete again.
New behaviour in v2.4
The installer has been moved from the install/update phase to the autoload-dump phase. It will now process the installation as a whole rather than one library at a time. This simplifies things a lot.reviously, the installer ran for each installed package and potentially did a few procedures multiple times. Both the installer and the horde-reconfigure command will now issue some output to the console about their operation and they will process the installation only once with the updated autoloader already configured. The changes will now also apply on removal of packages or on other operations which require a rewrite of the autoloader. The registry snippets now include comments explaining that they are autogenerated and how to override the autoconfigured values.
Outlook to 2.5 or 3.0
The composer API has improved over the last year. We need to be reasonably conservative to support OS distribution packaged older versions of composer. At some point in the future however I want to have a look at using composer for simplifying life
Improve Theme handling: Listing themes and their scope (global and app specific), setting default theme of an installation
Turning a regular installation into a development setup for specific libraries or apps
Properly registering local packages into composer’s package registry and autoloader (useful for distribution package handling).
Both composer’s native APIs and the installer plugin can support improving a horde admin’s or developer’s life:
Make horde’s own “test” utility leverage composer to show which optional packages are needed for which drivers or configurations
Expose some obvious installation health issues on the CLI.
Only expose options in the config UI which are supported by current PHP extensions and installed libraries
Expose a check if a database schema upgrade is needed after a composer operation, both human readable and machine consumable. This should not autorun.
The actual feature code may be implemented in separate libraries and out of scope for the installer itself. As a rule, horde is supposed to be executable without composer but this is moving out of focus more and more.
On recent OS distributions, two conflicting changes can bring trouble.
MariaDB refuses connections with ‘utf-8’ encoding
Recent MariaDB does not like the $conf[‘sql’][‘charset’] default value of ‘utf-8’. It runs fine if you change to the more precise ‘utf8mb4’ encoding. This is what recent MySQL understands to be ‘utf-8’. You could also use ‘utf8mb3’ but this won’t serve modern users very well. The ‘utf8m3’ value is what older MariaDB and MySQL internally used when the user told it to use ‘utf-8’. But this character set supports only a subset of unicode, missing much-used icons like ββββπ³πβ‘β which might be used anywhere from calendar events sent out by travel agencies to todos or notes users try to save from copy/pasted other documents.
I have changed the sample deployment code to use utf8mb4 as the predefined config value.
Shares SQL driver does not understand DB-native charsets
The Shares SQL driver does some sanitation and conversion when reading from DB or writing to DB. The conversion code does not understand DB native encodings like “utf8mb4”. I have applied a patch to the share library that would detect and fix this case but I am not satisfied with this solution. First, this issue is bound to pop up in more places and I wouldn’t like to have this code in multiple places. Either the DB abstraction library horde/db or the string conversion library in horde/util should provide a go-to solution for mapping/sanitizing charset names. Any library using the config value should know that it needs to be sanitized but should not be burdened with the details. I need to follow up on this.
As you might remember from a previous post, Horde Core’s design is more complex than necessary or desirable for two main reasons:
Horde predates today’s standards like the Composer Autoloader and tries to solve problems on its own. Changing that will impair Horde’s ability to run without composer which we were hesitant to do, focusing on not breaking things previously possible.
Horde is highly flexible, extensible and configurable, which creates some chicken-egg problems to solve on each and every call to any endpoint inside a given app.
Today’s article concentrates on the latter problem. More precisely, we want to make routing more straight forward when a route is called.
A typical standalone or monolithic app usually is a composer root package. It knows its location relative to the autoloader, relative to the dependencies and relative to the fileroot of the composer installation. Moreover, the program usually knows about all its available routes. It will also have some builtin valid assumptions about how its different parts’ routes relate to the webroot. None of this is true with a typical composer-based horde installation.
None of the apps is the root package
Apps are exposed to a web-readable subdir of the root package
While the relative filesystem location is known, each app can live in a separate subdomain, in the webroot or somewhere down the tree
Each app may reconfigure its template path, js path, themes path
The composer installer plugin makes a sane default. This default can be overridden
Each app can be served through multiple domains / vhosts with different registry and config settings in the same installation
Administrators can add local override routes
Parts of the code base rely on horde’s own runtime-configurable autoloader rather than composer.
This creates a lot of necessary complexity. The router needs to know the possible routes before it can map a request. To have the routes, the context described above must be established. Complexity cannot be removed without reducing flexibility. There is, however, a way out. The routing problem can be divided into three phases with different problems:
Development time – when routes are defined and changed frequently for a given app or service
Installation/Configuration time – when the administrator decides which apps’ routes will be available for your specific installation
Runtime – when a request comes in and the router must decide which route of which app needs to react – or none at all
Let’s ignore development time for now. It is just a complication of the other two cases. The design goal is to make runtime lean and simple. Runtime should initialize what the current route needs to work and as little as possible on top of that. Runtime needs to know all the routes and a minimal setup to make the router work. Complexity needs to be offloaded into installation time. Installation time needs to create a format of definite routes that runtime can process without a lot of setup and processing. As a side effect, we can gain speed for each individual call.
Modern Autoloaders are similar in concept: They have a setup stage where all known autoloader rules of the different packages are collected. In composer, the autoloader is re-collected in each installation or update process. The autoloader is exposed through a well-known location relative to the root package, vendor/autoload.php – it can be consumed by the application without further runtime setup. The autoloader can be optimized further by processing the autoloading rules into a fixed map of classes to filenames (Level 1), making these maps authoritative without an attempt to fail over on misses (Level 2a) and finally caching hits and misses into the in-memory APCu opcache. Each optimization process makes the lookup faster. This comes at the cost of flexibility. The mapping must be re-done whenever the installation changes. Otherwise things will fail. This is OK for production but it gets in the way of development. The same is true for the router.
The best optimization relies on the application code and configuration being static. The list of routes needs to be refreshed on change. Code updates are run through composer. The composer installer plugin can automatically refresh the router. Configuration updates can happen either through the horde web ui or through adding/editing files into the configuration area. Admins already know they need to run the composer horde-reconfigure command after they added new config files or removed files. Now they also need to run it when they changed file content. In development, routing information may change on the fly multiple times per hour. Offering a less optimized, more involved version of this route collection stage can help address the problem.
A new version of the RampageBootstrap codebase in horde/core is currently in development. It will offload more of horde’s early initialisation stages into a firmware stack and will reduce the early initialisation to the bare minimum. At the moment, I am still figuring out how we can do this in a backward compatible way.