bookmark_borderHorde Installer: Recent Changes

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.

bookmark_borderMaintaina/Horde UTF-8 on PHP 8

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.

Update

See https://github.com/horde/Util/commit/7019dcc71c2e56aa3a4cd66f5c81b5273b13cead for a possible generalized solution.

bookmark_borderA new phase in life

TL;DR – I changed job and this will not affect ongoing maint. of anything Horde

I spent almost my whole work life with a single employer. It was quite a trip. I was part of it as a company grew from a hand full of guys into fifty, then hundred and ever more. I saw how structures developed, how people grew with their tasks and how a brand recognition built. It was a great time and I took advantage of all the opportunities and challenges that came along with it. How many places I traveled. The excitement of speaking at conferences, being a trainer, leading teams, designing architectures.

But after almost 15 years I am at a point where things need to change. Family is a priority now in a different way. Home has a different meaning. I needed a clean break. A few days ago I started a new job with one of Europe’s most relevant software companies. So far everything is shiny and new – I like it.

A little change comes along with it, too. Work life will not involve anything PHP or Horde anymore. There is this new, clear distinction between these things I do for fun or out of private interest on the one hand and earning money on the other. You cannot hire me for freelance work.

Nobody needs to worry. The Maintaina Horde fork is not going away. Development work on PHP 8.1 compatibility and features has not stopped.

Commercial Horde support at B1 Systems will still be around. I had the pleasure to work with an excellent team and that team is well capable of keeping up the expected quality and response times.
If you need any work done for hire, contact that company.

It’s an exciting summer after a pandemic winter. I will possibly take some weeks outside of mailing lists and bug reports to concentrate on things I must do now and things I like to do now. This is going to be fun. Stay tuned for updates.

bookmark_borderNet_DNS2 PHP 8.x compat issue

TLDR: When using horde/mail_autoconfig or other features using pear/net_dns2 under PHP 8.x, use the “master” branch or wait for a release of version 1.5.3 or higher.

While upgrading the Maintaina Horde codebase for PHP 8.1, I stumbled upon a problem:

[Sun May 15 12:15:15.026499 2022] [php:error] [pid 156] [client 172.23.0.1:43894] PHP Fatal error:  Uncaught ValueError: fread(): Argument #2 ($length) must be greater than 0 in /srv/www/horde/vendor/pear/net_dns2/Net/DNS2/Cache/File.php:147\nStack trace:\n#0 /srv/www/horde/vendor/pear/net_dns2/Net/DNS2/Cache/File.php(147): fread()\n#1 [internal function]: Net_DNS2_Cache_File->__destruct()\n#2 {main}\n  thrown in /srv/www/horde/vendor/pear/net_dns2/Net/DNS2/Cache/File.php on line 147, referer: http://localhost/horde/admin/config/config.php

It turns out Net_DNS2 as of latest version 1.5.2 has a problem in its filesystem cache when run under PHP 8.x – it tries to read the cache file content even if the file size is 0 or the file does not yet exist. In PHP 8.x this yields a ValueError because reading with a length 0 should not be attempted.

Mike Pultz, the maintainer of Net_DNS2 already has a fix in his master branch. I have kindly asked him to release a stable version with this fix. However I understand it may take a while or may not be very high on the daily priority list of todos. I am not sure what other changes the master branch contains which might impair its usefulness to your use case. I am hesitant to put work into a problem somebody else already fixed. I will only provide a downstream-packaged version in case no update happens within reasonable time.

bookmark_borderMaintaina Horde: Tumbleweed and PHP 8.1

PHP 8.1 is available off the shelf in openSUSE Tumbleweed. I will shortly prepare a PHP 8.1 / tumbleweed version of the maintaina Horde containers. These will initially be broken due to some outdated language constructs. As PHP 7.4 will EOL by the end of this year, I decided not to bother with PHP 8.0 and ensure compatibility with PHP 8.1 right away, while staying compatible with PHP 7.4 until end of year. This is not fun. PHP 8.x provides several features which allow for more concise code. I will not be able to use them.
This also means that for the time being I will produce code which you may find more verbose than necessary. While Constructor promotion is mostly about being less verbose, Readonly Properties and Enums kill some of the pro-method arguments in the eternal discussion if getter methods or public properties are more appropriate interfaces. Union Types and Intersection Types allow a flexibility of method interfaces which PHP 7.4 can only emulate. You can get far by type hints for static analysis combined with boilerplate guard code inside a method and dropping type hints all along or using insufficient surrogate interfaces. But it is really not shiny. Maintaining software which shows its age has its tradeoffs.

bookmark_borderSimplifying Routing / PSR-15 bootstrap in Horde

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.

bookmark_borderMaking horde/core more versatile

Hello, you may have seen my blog go quiet for a while. After two years of a global pandemic, I decided to take some more time with my family and also offload some knowledge and responsibilities around the Maintaina Horde codebase to my fellow team members at B1 Systems. I also took some time considering some very fundamental Horde design implications. One of these is horde/core.

You can use the Horde Framework and its libraries in two different styles. The first style I would call loosely coupled. Most libraries try to depend on as few as possible other horde libraries and concentrate on specific purposes. That makes them reusable outside of the wider context of the framework. For example, the backend for Michael Rubinsky’s blog uses the Horde Router, the Horde Controller library and the Horde Injector DIC as a kind of micro framework without much supporting code. A custom binder simulates setup of view paths normally performed by a horde environment.

The alternative is the “horde app” use case or tightly integrated use case. Your site will have a horde base app providing common capabilities and one or multiple modules or horde apps. They integrate with a framework-generated topbar, can use an inter-app API, react to a common styling choice etc. Developers benefit from being able to reuse solutions for preference storage and UI, configuration, permissions, user groups, predefined callbacks for webdav, rpc, language and presentation handling etc. This is all great if you just want to build some addon to your Horde Groupware installation. In other cases, Horde’s base does a lot of stuff you do not really need or want. I want to change that.

The horde/core library started out as a spinoff off the horde base app. It contains a lot of glue for putting together backends, drivers, config files, caches and it also provides base classes needed by the horde applications. The horde base app contains the endpoints for RPC, route-based UI, webdav, caldav. It also contains a micro bootstrapping file called horde/lib/core.php. This file installs an error handler, sets up some basic PHP sanity stuff, does some magic around Horde’s own autoloader, defines some constants and hooks into horde/config/horde.local.php, allowing the admin to inject some early-init custom magic. Our horde-installer-plugin for composer makes use of this hook to setup the composer autoloader (and some more constants) in a backward compatible way.

I think, outside of support for old-style code bases, none of this should happen. But our current implementation of the controller framework still depends on that magic and uses code in horde/core which also depends on it. In pre-composer environments, horde needs some tricks to find out basic facts about where it is, where everything else is, how to setup autoloading etc. In modern environments, we should neither pollute the global namespace with constants and global variables nor should we have an involved, tightly coupled setup process way before we even look at the app and route called for. We need to reorder how and when things are done.

Both developers and runtime operators benefit from this redesign. Developers can reduce boilerplate when relying on much, but not all of the Horde Framework and not exactly building a module for a horde installation. They are less entangled in conventions which do not make sense anymore. Operators will notice a lower memory, i/o and computation footprint, resulting in higher speed. This is because we throw out a lot of overhead unrelated to the current call. In the old setup we even created an IMAP connection when the current user was forbidden to use the mail component and the screen shown was for changing passwords or handling preferences. Yes, even when downloading an addressbook you would have triggered and IMAP connection handshake. We do not need that and by now, we have to tools to avoid it.

Stay tuned for more details in upcoming articles.

bookmark_borderRdo: Persistence is not your model

Remember that post on how your backend might betray you? You can store your Turba addressbook into an LDAP tree, but if the addressbook is manipulated from LDAP side, your CardDAV Sync may be ignorant of this. The bottom line is: You cannot trust the backend. Nor should the persistence model govern your application internal model.

The development team at B1 works with a lot of inherited code from different areas and eras of FOSS and closed-source development. We see all styles of code and we have made all sorts of bad micro decisions ourselves over the years. One particulary hard question that comes up time and again is what is the “true” model of data in an application. Developers who come from traditional, monolithic web applications may say the true model is what is in the (relational) database and point out that it will closely influence the application’s internal model. Developers with a background in APIs and distributed apps will tend to think the messages going in and out of a service are the main thing. Application will be modeled after the messages and persistence is a secondary thought. Then we have the school of DDD-trained developers who will say neither is right. The internal model should be mostly ignorant of persistence and external API is just another type of persistence.

Let’s explore the benefits and impacts of these options.

DB-centric applications

In a DB-centric application, your entities closely represent rows of data in DB tables. If you use ActiveRecord or DataMapper style ORMs, you will often use the ORM Entities as your business objects in the application. This works fairly well for CRUD style applications where most if not all fields in the database will be editable form fields on screen.

If your application does little beyond storing, retrieving, updating and deleting “records” of whatever type, you will have a straight-forward development experience. Some frameworks may support autogenerating create/view/edit forms and searchable lists (html tables) out of your DB schema. Because it is the easiest way to add features to these types of application, the object model tends to be rich in attributes and limited in relations and segregation.

For example, it would be common for a “customer” entity to include separate fields for the customer’s street, city, country, email address… some fields would be marked as mandatory and others as optional, each accessible via getters and setters or public properties. This becomes cumbersome for entities which have subtypes with different sets of attributes or behaviours. Fields may be mandatory for one subtype, but optional or even forbidden for other subtypes. Attribute values are usually primitives (string, int) that can be mapped to DB column types easily. Developing these types of apps is really fast and concise as long as you do nothing fancy. As soon as you have any real amount of business logic and variance, it becomes cumbersome to maintain and hard to test.

API-centric applications

A similar school of thoughts comes out of microservice development. In many cases, CRUD services can be prototyped from autogenerated code defined through a Swagger/OpenAPI definition file. Sometimes there is little to do after this autogeneration, including persistence to the relational database. If you use a non-relational persistence like CouchDB, you may even get along with some variance and depth in the schema. Even a json or yaml file on disk gets you very far (at least in the prototype stage).

However, you will have scenarios where the same application object looks quite different between API requests. Users may have limited permissions on querying details of an object, different APIs with different use cases may have limited needs on the deeper details of objects. Retrieving your list of customer IDs and customer names for populating a dropdown is different from being able to view their billing data or contact persons’ details.

Domain Model centric approach

If your application has to deal with structured aggregates and consistency rules, exhibits rich behaviour, services multiple types of backends or is otherwise complicated, this might be the right approach.

At its core, the (micro)application is fairly ignorant of both persistence, export formats and external API.
The domain model deviates from persistence model or message representations. It has internal constraints and business rules. A domain entity is retrieved from repository in valid and complete state and all transformations will result in valid state. Transformations usually happen through defined operations rather than accessing a set of getters and setters. Setters may even not exist for many types of domain objects.

Through this, you can trust your objects by contract and reduce validations in the actual operations. This comes at a price: In a domain centric application, your classes may double or triple compared to other approaches.

You have your business object with all the internal integrity aspects. You have another version of the same object which is used for I/O – the so-called Data Transfer Object or DTO. There are versions where DTOs are either typed classes or untyped plain objects with just arbitrary public attributes. They may even just be structured arrays. Each option comes with a drawback. These world-readable objects expose all their relevant data to some kind of I/O – views rendered on the server side, files to export, REST API messages coming in or going out. A third version of your entity may be the persistence model(s), especially in relational databases. They may decompose your domain entity into multiple database tables, LDAP tree nodes or even multiple representations in no-sql databases. The main effort is getting all these transformations right. This type of application may seem verbose and repetitive. On the other hand, a strong object model with hard internal constraints and few dependencies on unrelated aspects is very straight-forward to unit test. This makes it suitable for large scale applications.

Compromises and practice

You do not have to commit to one approach with all consequences. This might even make less sense in languages which do not really enforce type hints (like python) or have no enforced concept of private and protected properties (again, like python). In many cases, you can get very far by discipline alone. Just restrict usage of setters to the persistence and I/O parts of your application. Add methods and behaviour to your persistence layer entities and even better, add an interface. Hint your business logic against the interface. You can also make your ORM mapper double as a Repository. You can scale out to real domain objects step by step as needed. Add conversion methods which turn a domain entity into one or many ORM entities and commit them to the DB. If your aggregate is not very suitable for this, hide your ORM mappers in a dedicated repository class. You can scale out as little or as much as your needs dictate.

On the other hand, you may abuse your ORM entities to populate views or data formats like XML, json or yaml.

Why we move away from putting logic into Rdo entities

Rdo is Rampage Data Objects, Horde’s minimalistic ORM solution. We have come a long way using the described tricks to make Horde_Rdo_Mappers work as repository implementations and Horde_Rdo_Base entities as business objects. However, we run into more and more situations where this is not appropriate. Using and lazy-loading relations to child objects is fine in the read use case, but persisting changes to such structures becomes tedious and error prone. Any logic tightly coupled to Rdo objects is also hard to unit test. Rdo-centric development does not mix well with the Shares library or with handling users and identities. As Rdo entities carry around references to the mapper and the database driver, they tend to bloat debug output. More fundamentally, our object models are evolving to a design which does not really look like database tables at all. We still love Rdo and may resort to Rdo entities with interfaces here and there, especially in prototyping. But our development model has long shifted towards unittest-early or unittest-driven rather.

Beware of the edges

Domain Driven Design purists may emphasize how domain models will rarely have attribute setters and some will even argue you should minimize your getters (tell don’t ask). However, at some point we need to get raw attributes to compose messages to the edges, to answer API requests or to persist any objects. There are multiple ways to do it, with different implications. Remember the Data Transfer Objects? I tend to have method on my domain aggregate to “eject” a fully detailed world readable object. In many cases, it is even typed. This chatty object can now be used for Formatters to transform them into API messages or data files. So the operation would be: Construct the domain aggregate – and fail if data is invalid – and from the domain aggregate, construct the DTO. Use the DTO for chatty jobs.

But how about the other way around? Messages from the outside can be malformed in many ways. They may be tampered with, they may miss details by design. If I exported an object to a user’s limited API and he sends an update, how to handle that? If an API user does not even know advanced attributes that are subject to other views and use cases, how would he create new entities?

I tend to have a method on the repository which accepts these messages and tries to turn them into Domain Objects. If the partial message contains some ID field or a sufficient set of data to form a unique key, I will first ask the backend to get the original details of the object and then apply the message details – and fail if this violates the constraits of my aggregate. If no previous version exists in the backend, missing information is added from defaults and generators, including a unique id of some sort. The message is applied on top.

The is approach may be expensive. I am creating full domain objects just to render database content to another format which may not even contain most of the retrieved data. On the other hand, if I know an object exists in a relational db, I could use some cheap SQL to apply partial changes without ever constructing the full model.

Creating straight-to-output repos with optimized queries is a tradeoff. They mean additional maintenance burden whenever the model changes, additional parts to cover with tests. I only do this when performance actually becomes an issue, not by default.

Creating straight-to-persistence methods for partial messages is even less desirable. It is circumventing integrity check at code level. However, sometimes the message itself is the artifact to persist – to be later processed by a queue or other asynchronous process.

bookmark_borderPHP 8 Horde (Maintaina)

Over the next few days, all Horde libraries and apps in the maintaina-com organization will be whitelisted for PHP 8x. in their FRAMEWORK_6_0 branch development versions. One next step will be a flavour of the OpenSUSE based containers and deployments which runs off PHP 8.0. While some few libraries have been enabled for PHP 8, it is almost certain that horde as a whole will not run correctly. Main culprits are the horde/rpc and horde/form packages and their user code, but there are some other ugly places that need attention.

Development Baseline at 7.4

Code in the maintaina-com repo will stay compatible with PHP 7.4 – at least for the time being. Decisions at Horde LLC may override that at some point or time may just march on. PHP 7.4 has been released two years ago, has ended active support 20 days ago and will be EOLed for upstream security support on November 28th 2022 – roughly 11 months to go. Linux distributions have a tradition to follow their own schedules and backport security fixes. OpenSUSE LEAP 15.3 ships with PHP 7.4 while openSUSE Tumbleweed has switched to PHP 8.0.13 – with PHP 8.1 versions becoming available from official repos soon.

This is a tough decision as PHP 8 and 8.1 have some really interesting features which would allow us to develop more elegant, more readable and more efficient code. For software that is not intended for this audience, I will immediately allow using 8.x-only features as soon as we are confident with Horde’s compatibility. This is going to be a major theme of January and possibly February.

No need to switch right now

If you are running Horde as of horde.org master branches or maintaina-com FRAMEWORK_6_0 branches off PHP 7.4, you should NOT switch right now. We will announce once we think any leftover issues are minor enough for an acceptable early adopter experience.

No particular love for 8.0.x

There is no guarantee our runtime will stay fixed at 8.0. PHP 8.1 offers a lot of new features and a considerable performance boost for some relevant scenarios. While making Maintaina Horde work with 8.x on a 7.4 feature baseline is the first step, the logical next step is upgrading feature baseline to 8.1 or higher. This will be much less of a problem if we get an official Horde 6 release in the meantime and users can choose between a properly conservative release version and a more adventurous Maintaina version. This is not something I have under control though. Horde LLC do as they find appropriate and sustainable and for many users, there is little reason to choose Maintaina over the official releases once we have a Horde 6 version that properly runs on recent PHP and supports Composer out of the box. I am perfectly fine with that and looking forward to it. I will always assist with a migration path as far as I can afford to.

Time is Money, Money buys Time

If you have an urgent commercial interest in a PHP 8-ready Horde version, you really do not want to rely on Maintaina’s timelines and priorities which may be subject to change. You will need to spend money. Approach somebody to do it for you, either Horde LLC or the company I work for, B1 Systems GmbH – both are formidable places to look for Horde-experienced development resources.

Update 2021-12-18 21:00 CET

I just ran the update to the metadata as a mass operation for everything which contains a .horde.yml file – the rest will have to wait until I stumble across it. I leveraged an edited version of horde/git-tools, some bash magic, some mass editing in vscode using their regex tool and some manual fixing.

  • All packages now formally require “php”: “^7.4 || ^8”
  • If horde-installer-plugin is required, I now go for “^2 || dev-FRAMEWORK_6_0” – however in maintaina-com/Core, I have a job that rebuilds composer.json on commit and this job showed me that the components tool needs an update in this aspect.
  • SPDX license code warnings for LGPL and GPL versions have been remedied to LGPL-2.0-only, LGPL-3.0-only, GPL-3.0-only each
  • Added the CI workflow where missing. Mostly it will fail until further editing. This is intentional.
  • I did NOT unify all versions of CI workflow as some deviations are intentional. I did however unify PHP versions for the unit tests to “7.4”, “8.0” and “latest” and I did unify phpunit versions to “9.5” and “latest”.
  • Unified/added the phpdoc workflow and the update-satis workflow as we had multiple versions for no good reason. I have settled for a version of the phpdoc job that will scan lib/, src/ and app/ if they exist
  • Cleaned up a lot of metadata mess in the Kolab related packages.
  • Removed some version: tags from composer.json files
  • Removed the optional pear dependency of imp for the ASN1 implementation from phpseclib – need to look for a proper composer-ready and less outdated replacement.

While the mass changes themselves seem to have gone right, the resulting avalanche of CI jobs showed some issues:

  • phpdoc job and update-satis job fail if they run in parallel and the satis repo content has changed since checkout. Either give the push commands in the loop a minute to wait each time or make the job smarter about handling these clashes. Still, failing is better than silently overwriting content
  • Having so many versions of the CI job is not maintainable. Need to factor out the boilerplate into an action, make version requirements a config variable with a builtin default and have some mechanism for there rare cases where extra software is needed for meaningful QA, i.e. database and storage related items.
  • After getting this migration done, upgrading the git-tools utility may be an interesting exercise in PHP 8 and PHPStan.
  • I may have created unnecessary conflicts with some open pull requests. Sorry, contributors. I will improve.

bookmark_borderHorde/Skeleton: Modernized

Over the last months, a lot of new technologies have entered the Horde ecosystem. It was long overdue to modernize the Skeleton example app.

No more un-namespaced code

Skeleton has completely migrated to PSR-4 namespaced code in /src/ rather than traditional, unnamespaced code in /lib/. This includes framework integration classes like Application, Api, Ajax\Application but also the portal blocks.

All application internal classes of skeleton are now served via the Composer Autoloader and follow the PSR-4 standard. This requires the very latest releases of horde/horde (6.0.0alpha6) and horde/core (v3.0.0alpha9) to work correctly. The only exception is the database schema migration which intentionally does not follow regular autoloading conventions.

No more index.php

Client pages that traditionally called into the application’s internal classes have been removed and replaced by routes. This includes the index.php file. A default route handles the skeleton/ and skeleton/index.php cases. The contents of the example UI have not been changed. They are only implemented differently

Using horde/routes and horde/http_server

The new skeleton uses horde/routes to describe available routes in the app and horde/http_server to implement the controller classes behind these routes. The code comes with extensive documentation comments. horde/http_server implements the PSR-15: HTTP Server Request Handlers standard used by most modern PHP Frameworks.

Inter-App API

Skeleton now includes an example of the inter-app API implemented through the registry. The same interface is used as the basis for json-rpc and XMLRPC APIs.

Full Backward Compatibility

The changed libraries still work with unnamespaced or partially converted apps. Implementers can work according to their own schedule. However, there are some rules to keep in mind:

  • The namespaced versions of Application, Api, Test and Ajax\Application take precedence over their unnamespaced counterparts. Implementers can leave the unnamespaced code as-is or turn it into wrappers like
    lib/Application.php
    
    <?php
    /**
     * Backward compatibility wrapper.
     *
     * @deprecated Call into Horde\Skeleton\Application directly instead. 
     */ 
    Skeleton_Application extends Horde\Skeleton\Application {}
    
    
    
  • Portal Blocks should NOT be wrapped or duplicated. They should exist as either namespaced or unnamespaced versions. You can have both types in the same application, but if you have a wrapped or copied block, it will show up twice.
  • Ajax application handler classes only have one integration point, /{lib, src}/Ajax/Application.php. You can upgrade them any time, just change the reference in the Application class. The Ajax Application class itself can be duplicated or wrapped, but the namespaced version will always be chosen. The wrapper would be just a transitional backwards compatibility measure so your application still works with earlier alpha versions of the framework.

Not in this release

The current version of skeleton still leaves room for improvement. Not all external libraries used in the code base are already namespaced or otherwise modernized. The PageOutput helper is still emitting output which needs to be caught and redirected to the output stream as part of the PSR-7 HTTP Response object. Future versions should use a stream-ready implementation to reduce boilerplate. Also, there should be some ready-made controllers for standard cases like UI output or REST.

References