Skip to content

Commit 2d61ff5

Browse files
committed
feat: add support for flavor selection
As a maintainer I need on a regular basis to pick specific server type to reproduce issues. But patching source code to do so is: a. cumbersome b. error prone And sometimes this can be useful for users as well (imagine you have a 8.4.5 installed with CLI only and 8.4.4 with FPM and CLI and your project requires FPM for instance). This also lays the ground to pick more flavors (debug or zts for example)
1 parent e76629f commit 2d61ff5

File tree

4 files changed

+202
-6
lines changed

4 files changed

+202
-6
lines changed

store.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,21 +143,32 @@ func (s *PHPStore) BestVersionForDir(dir string) (*Version, string, string, erro
143143
}
144144

145145
// bestVersion returns the latest patch version for the given major (X),
146-
// minor (X.Y), or patch (X.Y.Z). version can be 7 or 7.1 or 7.1.2.
147-
// Non-symlinked versions have priority
146+
// minor (X.Y), or patch (X.Y.Z).
147+
// Version can be 7 or 7.1 or 7.1.2 and optionally suffixed with a flavor.
148+
// Non-symlinked versions have priority.
149+
// If the asked version contains a flavor (e.g. "7.4-fpm"), it will only accept
150+
// versions supporting this flavor.
148151
// If the asked version is a patch one (X.Y.Z) and is not available, the lookup
149152
// will fallback to the last patch version for the minor version (X.Y).
150153
// There's no fallback to the major version because PHP is known to occasionally
151154
// break BC in minor versions, so we can't safely fall back.
152155
func (s *PHPStore) bestVersion(versionPrefix, source string) (*Version, string, string, error) {
153156
warning := ""
157+
flavor := ""
158+
159+
// Check if versionPrefix has a expectedFlavors constraint, if so first do an
160+
// exact match lookup and fallback to a minor version check
161+
if pos := strings.LastIndexByte(versionPrefix, '-'); pos != -1 {
162+
flavor = versionPrefix[pos+1:]
163+
versionPrefix = versionPrefix[:pos]
164+
}
154165

155166
// Check if versionPrefix is actually a patch version, if so first do an
156167
// exact match lookup and fallback to a minor version check
157168
if pos := strings.LastIndexByte(versionPrefix, '.'); pos != strings.IndexByte(versionPrefix, '.') {
158169
// look for an exact match, the order does not matter here
159170
for _, v := range s.versions {
160-
if v.Version == versionPrefix {
171+
if v.Version == versionPrefix && v.ForceFlavorIfSupported(flavor) {
161172
return v, source, "", nil
162173
}
163174
}
@@ -173,7 +184,7 @@ func (s *PHPStore) bestVersion(versionPrefix, source string) (*Version, string,
173184
// start from the end as versions are always sorted
174185
for i := len(s.versions) - 1; i >= 0; i-- {
175186
v := s.versions[i]
176-
if strings.HasPrefix(v.Version, versionPrefix) {
187+
if strings.HasPrefix(v.Version, versionPrefix) && v.ForceFlavorIfSupported(flavor) {
177188
return v, source, warning, nil
178189
}
179190
}

store_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package phpstore
22

33
import (
44
"path/filepath"
5+
"sort"
56
"testing"
67
)
78

@@ -17,6 +18,20 @@ func TestBestVersion(t *testing.T) {
1718
}
1819
}
1920

21+
{
22+
v := "8.0.26"
23+
ver := NewVersion(v)
24+
ver.PHPPath = filepath.Join("/foo", v, "bin", "php")
25+
ver.FPMPath = filepath.Join("/foo", v, "bin", "php-fpm")
26+
store.addVersion(ver)
27+
28+
if !store.IsVersionAvailable(v) {
29+
t.Errorf("Version %s should be shown as available", v)
30+
}
31+
}
32+
33+
sort.Sort(store.versions)
34+
2035
{
2136
bestVersion, _, _, _ := store.bestVersion("8", "testing")
2237
if bestVersion == nil {
@@ -56,4 +71,17 @@ func TestBestVersion(t *testing.T) {
5671
t.Error("8.0.99 requirement should not trigger a warning")
5772
}
5873
}
74+
75+
{
76+
bestVersion, _, warning, _ := store.bestVersion("8.0-fpm", "testing")
77+
if bestVersion == nil {
78+
t.Error("8.0-fpm requirement should find a best version")
79+
} else if bestVersion.Version != "8.0.26" {
80+
t.Errorf("8.0-fpm requirement should find 8.0.26 as best version, got %s", bestVersion.Version)
81+
} else if bestVersion.serverType() != fpmServer {
82+
t.Error("8.0-fpm requirement should find an FPM expectedFlavors")
83+
} else if warning != "" {
84+
t.Error("8.0-fpm requirement should not trigger a warning")
85+
}
86+
}
5987
}

version.go

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,20 @@ import (
3030
type serverType int
3131

3232
const (
33-
fpmServer serverType = iota
34-
cgiServer
33+
noServerType serverType = iota
3534
cliServer
35+
cgiServer
36+
fpmServer
3637
frankenphpServer
3738
)
3839

40+
const (
41+
FlavorCLI string = "cli"
42+
FlavorCGI string = "cgi"
43+
FlavorFPM string = "fpm"
44+
FlavorFrankenPHP string = "frankenphp"
45+
)
46+
3947
// Version stores information about an installed PHP version
4048
type Version struct {
4149
FullVersion *version.Version `json:"-"`
@@ -49,6 +57,8 @@ type Version struct {
4957
PHPdbgPath string `json:"phpdbg_path"`
5058
IsSystem bool `json:"is_system"`
5159
FrankenPHP bool `json:"frankenphp"`
60+
61+
typeOverride serverType
5262
}
5363

5464
func NewVersion(v string) *Version {
@@ -113,9 +123,14 @@ func (v *Version) IsFrankenPHPServer() bool {
113123
}
114124

115125
func (v *Version) serverType() serverType {
126+
// FrankenPHP is a special case as it will not support several server types
127+
// for a single installation.
116128
if v.FrankenPHP {
117129
return frankenphpServer
118130
}
131+
if v.typeOverride != noServerType {
132+
return v.typeOverride
133+
}
119134
if v.FPMPath != "" {
120135
return fpmServer
121136
}
@@ -126,6 +141,59 @@ func (v *Version) serverType() serverType {
126141
return cliServer
127142
}
128143

144+
func (v *Version) ForceFlavorIfSupported(flavor string) bool {
145+
if flavor == "" {
146+
return true
147+
}
148+
149+
if !v.SupportsFlavor(flavor) {
150+
return false
151+
}
152+
153+
switch flavor {
154+
case FlavorCLI:
155+
v.typeOverride = cliServer
156+
return true
157+
case FlavorCGI:
158+
v.typeOverride = cgiServer
159+
return true
160+
case FlavorFPM:
161+
v.typeOverride = fpmServer
162+
return true
163+
case FlavorFrankenPHP:
164+
// FrankenPHP installations does not support multiple flavors so there's
165+
// no need to set the typeOverride.
166+
return true
167+
}
168+
169+
return false
170+
}
171+
172+
func (v *Version) SupportsFlavor(flavor string) bool {
173+
if flavor == "" {
174+
return true
175+
}
176+
177+
serverFlavor := v.serverType()
178+
if serverFlavor == frankenphpServer {
179+
return flavor == FlavorFrankenPHP
180+
}
181+
182+
// CLI flavor is always supported
183+
if flavor == FlavorCLI {
184+
return true
185+
}
186+
187+
switch serverFlavor {
188+
case cgiServer:
189+
return flavor == FlavorCGI
190+
case fpmServer:
191+
return flavor == FlavorFPM
192+
}
193+
194+
return false
195+
}
196+
129197
func (v *Version) setServer(fpm, cgi, phpconfig, phpize, phpdbg string) string {
130198
msg := fmt.Sprintf(" Found PHP: %s", v.PHPPath)
131199
fpm = filepath.Clean(fpm)

version_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright (c) 2021-present Fabien Potencier <[email protected]>
3+
*
4+
* This file is part of Symfony CLI project
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as
8+
* published by the Free Software Foundation, either version 3 of the
9+
* License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
package phpstore
21+
22+
import (
23+
"testing"
24+
)
25+
26+
func TestVersion_SupportsFlavor(t *testing.T) {
27+
testCases := []struct {
28+
version *Version
29+
expectedFlavors []string
30+
}{
31+
{
32+
version: func() *Version {
33+
v := NewVersion("8.1")
34+
v.FPMPath = "/usr/bin/php-fpm8.1"
35+
v.PHPPath = "/usr/bin/php-8.1"
36+
return v
37+
}(),
38+
expectedFlavors: []string{FlavorFPM, FlavorCLI},
39+
},
40+
{
41+
version: func() *Version {
42+
v := NewVersion("8.2")
43+
v.CGIPath = "/usr/bin/php-cgi8.1"
44+
v.PHPPath = "/usr/bin/php-8.1"
45+
return v
46+
}(),
47+
expectedFlavors: []string{FlavorCGI, FlavorCLI},
48+
},
49+
{
50+
version: func() *Version {
51+
v := NewVersion("8.3")
52+
v.PHPPath = "/usr/bin/php-8.3"
53+
return v
54+
}(),
55+
expectedFlavors: []string{FlavorCLI},
56+
},
57+
{
58+
version: func() *Version {
59+
v := NewVersion("8.4")
60+
v.PHPPath = "/usr/bin/frankenphp"
61+
v.FrankenPHP = true
62+
return v
63+
}(),
64+
expectedFlavors: []string{FlavorFrankenPHP},
65+
},
66+
}
67+
for _, testCase := range testCases {
68+
if !testCase.version.SupportsFlavor("") {
69+
t.Error("version.SupportsFlavor('') should return true, got false")
70+
}
71+
for _, flavor := range testCase.expectedFlavors {
72+
if !testCase.version.SupportsFlavor(flavor) {
73+
t.Errorf("version.SupportsFlavor(%v) should return true, got false", flavor)
74+
}
75+
}
76+
flavorLoop:
77+
for _, possibleFlavor := range []string{FlavorCLI, FlavorCGI, FlavorFPM, FlavorFrankenPHP} {
78+
for _, flavor := range testCase.expectedFlavors {
79+
if flavor == possibleFlavor {
80+
continue flavorLoop
81+
}
82+
}
83+
84+
if testCase.version.SupportsFlavor(possibleFlavor) {
85+
t.Errorf("version.SupportsFlavor(%v) should return false, got true", possibleFlavor)
86+
}
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)