Composer/Replace

Aus Contao Community Documentation

MsgError.png Unvollständiger Artikel: dieser Artikel ist noch nicht sauber bearbeitet.

Bitte erweitere ihn und entferne erst anschliessend diesen Hinweis.

Wie composer mit replace umgeht

Mit der replace Eigenschaft kann man festlegen, dass das eigene Paket ein fremdes Paket "ersetzt". Dafür kann es mehrere Gründe geben, der häufigste ist wohl, dass das Paket unter einem anderen Namen weiter entwickelt wird, weil bspw. der Entwickler gewechselt hat. Im Contao Kontext sieht man auch sehr viel { "replace": { "contao-legacy/my-extension": "..." } }. Das sind replaces für Pakete, die über den Legacy ER2 Server in Composer bereitgestellt werden, vom Entwickler dann aber später als "echtes" Composer Paket weiter geführt werden.

Sehr weit verbreitet ist vor allem die Notation mit *:

{ "replace": { "contao-legacy/my-extension": "*" } }

Im folgenden soll erläutert werden, warum ein replace auf alle Versionen wie in dem vorhergehenden Beispiel die denkbar schlechteste Variante ist, die man sich vorstellen kann. Dazu muss man erst ein mal verstehen, wie replace funktioniert.

Wir gehen einfach mal davon aus, wir haben die folgenden 3 Pakete:

{
	"name": "test/old",
	"version": "1.0",
	"dist": {
		"url": "...",
		"type": "zip"
	},
	"source": {
		"url": "...",
		"type": "svn",
		"reference": "master"
	}
}
{
	"name": "test/old",
	"version": "1.1",
	"dist": {
		"url": "...",
		"type": "zip"
	},
	"source": {
		"url": "...",
		"type": "svn",
		"reference": "master"
	}
}
{
	"name": "test/new",
	"version": "2.0",
	"dist": {
		"url": "...",
		"type": "zip"
	},
	"source": {
		"url": "...",
		"type": "svn",
		"reference": "master"
	},
	"replace": {
		"test/old": "*"
	}
}

Um die gültigen Pakete zu finden, arbeitet Composer intern mit einem Pool. In diesen Pool generiert Composer für jeden Paketnamen eine sortierte Liste der Pakete erzeugt, die für den Paketnamen gültig sind.

Für das Paket test/old sieht diese Liste folgend aus:

  • test/old 1.0
  • test/old 1.1
  • test/new 2.0

Wie Composer darauf kommt, dass test/new in diese Liste kommt ist ganz einfach. Jedes Paket verfügt nicht nur über seinen eindeutigen Namen, sondern auch über Alias-Namen. Schaut man sich die BasePackage::getNames() Methode an, wird schnell klar welche Namen für ein Paket akzeptiert werden. Abgesehen vom Package::$name werden auch noch alle Package::$provides und Package::$replaces als gültige Namen für das Paket betrachtet.

Unter Strich heißt dass, dass die Namensliste von test/new aus [test/new, test/old] besteht und test/new deshalb in der Paketliste zu test/old (2 Absätze zuvor) auftaucht.

Wir gehen davon aus, wir wollen mit { "require": { "test/old": "~1.0" } } das Paket test/old in irgendeiner 1er Version installieren (Hinweis: ~1.0 ist gleichbedeutend mit >=1,<2-dev).

Betrachtet man jetzt die Auswertungslogik in der Pool::computeWhatProvides() Methode welche die Paketliste durchgeht und das höchst-versionierte (letzte) gültige Paket auswählt, kommt man zu folgendem Ergebnis:

  • Paket test/old in Version 1.0 matched über seinen Namen und seine Version
  • Paket test/old in Version 1.0 matched ebenfalls über seinen Namen und seine Version
  • Paket test/new in Version 2.0 matched über sein replace, außerdem matched die Version * auf die Constraint ~1.0

Damit wird test/new als gültiges Paket akzeptiert und weil es die jüngste Versionsnummer hat, wird dieses auch installiert.

Intern macht Composer also innerhalb der Pool::match() Methode eine Prüfung, ob die Version * auf die Constraint ~1.0 zutrifft. Und weil * immer zutrifft, ist es also völlig egal was man als Constraint auswählt. Dem Nutzer wird quasi jede Chance genommen, das Paket test/old in einer von ihm vorgegebenen Version zu installieren!

Lösung: Anstelle des Wildcards, verwendet man einfach { "replace": { "contao-legacy/my-extension": "self.version" } }!

{
	"name": "test/new",
	"version": "2.0",
	"dist": {
		"url": "...",
		"type": "zip"
	},
	"source": {
		"url": "...",
		"type": "svn",
		"reference": "master"
	},
	"replace": {
		"test/old": "self.version"
	}
}

Die Constraint self.version wird intern durch die Version des ersetzenden Pakets ersetzt. In Pool::match() wird jetzt also geprüft ob die Version 2.0 auf die Constraint ~1.0 zutrifft, was natürlich fehl schlägt.

Hätten wir jetzt allerdings ein Paket test/new in Version 1.2, welches als Nachfolger von test/old Version 1.1 fungiert, wäre wieder alles in Ordnung, weil die Version 1.2 auf die Constraint ~1.0 matched. Im Regelfall ist es genau dass, was wir wollen!

TL;DR

Ein { "name": "me/my-extension", "replace": { "contao-legacy/my-extension": "*" } } sorgt dafür, dass jede Version des alten Paketes, durch das neue ersetzt wird. Das bedeutet aber auch, dass eine potentiell inkompatible Version 3 von me/my-extension installiert wird, auch wenn mit { "require": { "contao-legacy/my-extension": "1.0" } } die Version 1 von contao-legacy/my-extension angefordert wird. Das schlimmste: Als Benutzer kann man (fast) nichts dagegen machen!

Vereinfacht ausgedrückt ist ein replace nichts weiter, als ein Alias auf das Paket in dem der replace definiert wurde:

test/old 1.0
test/old 1.1
test/old *   alias for test/new 2.0


Deshalb sollte immer { "name": "me/my-extension", "replace": { "contao-legacy/my-extension": "self.version" } } verwendet werden!

Sonderfälle

Natürlich gibt es Sonderfälle, diese bedingen aber in der Regel die Angabe einer expliziten Version bspw. { "replace": { "contao-legacy/my-extension": "1.1" } }. In diesem Fall muss der Entwickler darauf achten, seinen replace Eintrag akribisch mit zu führen, bei steigenden Versionsnummern. Von der Verwendung von Wildcards, auch wenn es nur eine Major- oder Release-Wildcard bspw. { "replace": { "contao-legacy/my-extension": "1.1.*" } } ist, wird grundsätzlich abgeraten!

Ansichten
Meine Werkzeuge

Contao Community Documentation

Andreas, leg dich da hinten hin und schlaf, dann kommen wir vorwaerts.

Tristan Lins
Navigation
Verstehen
Verwenden
Entwickeln
Verschiedenes
Werkzeuge