From 07659450d31606b7f254a54ab97d7ce6524660a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Svita=C4=8D?= Date: Sun, 31 May 2026 12:08:13 +0200 Subject: [PATCH] add support for Sylius --- README.md | 1 + fixture/sylius/.env | 12 ++ fixture/sylius/composer.lock | 12 ++ .../sylius/vendor/sylius/sylius/composer.json | 5 + platform.go | 7 ++ shopware6.go | 60 +-------- shopware6_config_test.go | 2 + sylius.go | 41 +++++++ sylius_config_test.go | 52 ++++++++ symfony.go | 115 ++++++++++++++++++ 10 files changed, 250 insertions(+), 57 deletions(-) create mode 100644 fixture/sylius/.env create mode 100644 fixture/sylius/composer.lock create mode 100644 fixture/sylius/vendor/sylius/sylius/composer.json create mode 100644 sylius.go create mode 100644 sylius_config_test.go create mode 100644 symfony.go diff --git a/README.md b/README.md index 729d0f0..1205760 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,4 @@ Go config parsers for several PHP eCommerce platforms: - Shopware 5 / 6 - OpenCart 4 - JTL-Shop +- Sylius diff --git a/fixture/sylius/.env b/fixture/sylius/.env new file mode 100644 index 0000000..c1d6491 --- /dev/null +++ b/fixture/sylius/.env @@ -0,0 +1,12 @@ +# in all environments, the following files are loaded if they exist, +# the latter taking precedence over the former + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=REDACTED +###< symfony/framework-bundle ### + +###> doctrine/doctrine-bundle ### +# real PaaS deployments inject DATABASE_URL as an env var, overriding this line +DATABASE_URL=mysql://root@127.0.0.1/sylius_%kernel.environment%?serverVersion=8.0&charset=utf8mb4 +###< doctrine/doctrine-bundle ### diff --git a/fixture/sylius/composer.lock b/fixture/sylius/composer.lock new file mode 100644 index 0000000..d47c0c5 --- /dev/null +++ b/fixture/sylius/composer.lock @@ -0,0 +1,12 @@ +{ + "packages": [ + { + "name": "sylius/sylius-rector", + "version": "v1.0.0" + }, + { + "name": "sylius/sylius", + "version": "v1.14.4" + } + ] +} diff --git a/fixture/sylius/vendor/sylius/sylius/composer.json b/fixture/sylius/vendor/sylius/sylius/composer.json new file mode 100644 index 0000000..e51f6fa --- /dev/null +++ b/fixture/sylius/vendor/sylius/sylius/composer.json @@ -0,0 +1,5 @@ +{ + "name": "sylius/sylius", + "type": "library", + "description": "Modern e-commerce for Symfony." +} diff --git a/platform.go b/platform.go index 1970786..e0b3494 100644 --- a/platform.go +++ b/platform.go @@ -111,6 +111,13 @@ var AllPlatforms = []PlatformInterface{ "includes/config.JTL-Shop.ini.php", }, }, + &Sylius{ + basePlatform{ + "Sylius", + ".env", + "vendor/sylius/sylius/composer.json", + }, + }, } func (b *basePlatform) Name() string { diff --git a/shopware6.go b/shopware6.go index e7e09e9..c8d4e72 100644 --- a/shopware6.go +++ b/shopware6.go @@ -2,50 +2,18 @@ package gocommerce import ( "context" - "errors" - "fmt" - "os" "path/filepath" "regexp" - "strconv" ) type Shopware6 struct { basePlatform } -const ( - // DATABASE_URL=mysql://db-user-1:rhPb5xC2242444mFZDB@localhost:3306/db-1 - DBURL = `(?m)^\s*DATABASE_URL\s*="?\s*mysql://(.+?):(.+?)@(.+?):(\d+)/(.+?)"?$` -) - var sw6ComposerRgx = regexp.MustCompile(`shopware\/core`) func (s *Shopware6) ParseConfig(cfgPath string) (*StoreConfig, error) { - data, err := os.ReadFile(cfgPath) - if err != nil { - return nil, err - } - - m := regexp.MustCompile(DBURL).FindStringSubmatch(string(data)) - if len(m) != 6 { - return nil, fmt.Errorf("could not parse DSN in Shopware config path %s", cfgPath) - } - - port, err := strconv.Atoi(m[4]) - if err != nil { - return nil, err - } - - return &StoreConfig{ - DB: &DBConfig{ - Host: m[3], - User: m[1], - Pass: m[2], - Name: m[5], - Port: port, - }, - }, nil + return symfonyParseConfig(cfgPath) } func (s *Shopware6) Version(docroot string) (string, error) { @@ -57,28 +25,6 @@ func (s *Shopware6) BaseURLs(ctx context.Context, docroot string) ([]string, err if err != nil { return nil, err } - - db, err := ConnectDB(ctx, *cfg.DB) - if err != nil { - return nil, err - } - - rows, err := db.Query(`SELECT url FROM sales_channel_domain WHERE url NOT LIKE '%headless%'`) - if err != nil { - return nil, err - } - - urls := []string{} - for rows.Next() { - var url string - if err := rows.Scan(&url); err == nil { - urls = append(urls, url) - } - } - - if len(urls) > 0 { - return urls, nil - } - - return nil, errors.New("base url(s) not found in database") + return symfonyColumnURLs(ctx, cfg, + `SELECT url FROM sales_channel_domain WHERE url NOT LIKE '%headless%'`) } diff --git a/shopware6_config_test.go b/shopware6_config_test.go index ebb546e..0e21319 100644 --- a/shopware6_config_test.go +++ b/shopware6_config_test.go @@ -5,6 +5,7 @@ import ( ) func TestConfigToDSN(t *testing.T) { + t.Setenv("DATABASE_URL", "") sw6 := Shopware6{} want := DBConfig{ Host: "localhost", @@ -20,6 +21,7 @@ func TestConfigToDSN(t *testing.T) { } func TestConfigWithQuotesToDSN(t *testing.T) { + t.Setenv("DATABASE_URL", "") sw6 := Shopware6{} want := DBConfig{ Host: "localhost", diff --git a/sylius.go b/sylius.go new file mode 100644 index 0000000..aa0316d --- /dev/null +++ b/sylius.go @@ -0,0 +1,41 @@ +package gocommerce + +import ( + "context" + "path/filepath" + "regexp" +) + +type Sylius struct { + basePlatform +} + +// anchored so it matches sylius/sylius but not sylius/sylius-rector et al. +var syliusComposerRgx = regexp.MustCompile(`^sylius/sylius$`) + +func (s *Sylius) ParseConfig(cfgPath string) (*StoreConfig, error) { + return symfonyParseConfig(cfgPath) +} + +func (s *Sylius) BaseURLs(ctx context.Context, docroot string) ([]string, error) { + cfg, err := s.ParseConfig(filepath.Join(docroot, s.ConfigPath())) + if err != nil { + return nil, err + } + + // sylius_channel stores bare hostnames, not full URLs + hosts, err := symfonyColumnURLs(ctx, cfg, + `SELECT hostname FROM sylius_channel WHERE enabled = 1 AND hostname IS NOT NULL`) + if err != nil { + return nil, err + } + + for i, h := range hosts { + hosts[i] = "https://" + h + "/" + } + return hosts, nil +} + +func (s *Sylius) Version(docroot string) (string, error) { + return getVersionFromComposer(docroot, syliusComposerRgx) +} diff --git a/sylius_config_test.go b/sylius_config_test.go new file mode 100644 index 0000000..f39b953 --- /dev/null +++ b/sylius_config_test.go @@ -0,0 +1,52 @@ +package gocommerce + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// .env holds a %kernel.environment% placeholder DSN plus a sibling +// DATABASE_URL_MAGENTO and a commented pgsql line; only the mysql +// DATABASE_URL is picked and %kernel.environment% resolves via APP_ENV=dev. +func TestSyliusConfig(t *testing.T) { + t.Setenv("DATABASE_URL", "") + t.Setenv("APP_ENV", "") + + syl := platformByName(t, "Sylius") + dbc := dbConfigFromSource(t, fixtureBase+"sylius/.env", syl) + assert.Equal(t, + "root:@tcp(127.0.0.1:3306)/sylius_dev?allowOldPasswords=true", + dbc.DSN()) +} + +// a real DATABASE_URL env var (PaaS injection) overrides the .env file. +func TestSyliusConfigEnvOverride(t *testing.T) { + t.Setenv("DATABASE_URL", "mysql://u5er:p4ss@db.example.net:10239/randomdb") + + syl := platformByName(t, "Sylius") + dbc := dbConfigFromSource(t, fixtureBase+"sylius/.env", syl) + assert.Equal(t, + "u5er:p4ss@tcp(db.example.net:10239)/randomdb?allowOldPasswords=true", + dbc.DSN()) +} + +func TestSyliusVersion(t *testing.T) { + syl := platformByName(t, "Sylius") + ver, err := syl.Version(fixtureBase + "sylius") + assert.NoError(t, err) + assert.Equal(t, "1.14.4", ver) // anchored regex skips sylius/sylius-rector +} + +func TestSyliusDiscovery(t *testing.T) { + store := FindStoreAtRoot(fixtureBase + "sylius") + assert.NotNil(t, store) + assert.Equal(t, "Sylius", store.Platform.Name()) +} + +func TestSyliusUniquePath(t *testing.T) { + store := FindStoreAtUniquePath( + fixtureBase + "sylius/vendor/sylius/sylius/composer.json") + assert.NotNil(t, store) + assert.Equal(t, "Sylius", store.Platform.Name()) +} diff --git a/symfony.go b/symfony.go new file mode 100644 index 0000000..1b4aa50 --- /dev/null +++ b/symfony.go @@ -0,0 +1,115 @@ +package gocommerce + +import ( + "context" + "errors" + "fmt" + "os" + "regexp" + "strconv" + "strings" +) + +// Shared helpers for Symfony-based platforms (Shopware 6, Sylius, ...). +// They read the database connection from a Symfony DATABASE_URL, either from a +// real environment variable or from .env file. + +var ( + // matches a mysql DATABASE_URL, ignoring commented lines and the + // DATABASE_URL_ variants, with optional password, optional port + // and optional ?query parameters. The DATABASE_URL= prefix is optional so + // a bare DSN (from the environment) parses with the same expression. + symfonyDBURLRgx = regexp.MustCompile( + `(?m)^\s*(?:DATABASE_URL\s*=\s*)?"?(?:pdo-)?mysql://([^:@/]+)(?::([^@]*))?@([^:/?]+)(?::(\d+))?/([^?"\s]+)`) + + symfonyAppEnvRgx = regexp.MustCompile(`(?m)^\s*APP_ENV\s*=\s*"?([a-zA-Z0-9_-]+)`) +) + +// symfonyParseConfig parses a Symfony DATABASE_URL into a StoreConfig. A real +// DATABASE_URL environment variable takes precedence over the .env file, +// mirroring Symfony's own dotenv override behaviour. +func symfonyParseConfig(cfgPath string) (*StoreConfig, error) { + if dsn := os.Getenv("DATABASE_URL"); dsn != "" { + if db := parseSymfonyDSN(dsn, kernelEnv("")); db != nil { + return &StoreConfig{DB: db}, nil + } + } + + data, err := os.ReadFile(cfgPath) + if err != nil { + return nil, err + } + + db := parseSymfonyDSN(string(data), kernelEnv(string(data))) + if db == nil { + return nil, fmt.Errorf("could not parse mysql DATABASE_URL in %s", cfgPath) + } + return &StoreConfig{DB: db}, nil +} + +// parseSymfonyDSN extracts mysql connection details from a string holding a +// DATABASE_URL assignment or a bare DSN. env, when set, replaces Symfony's +// %kernel.environment% placeholder in the database name. +func parseSymfonyDSN(s, env string) *DBConfig { + m := symfonyDBURLRgx.FindStringSubmatch(s) + if m == nil { + return nil + } + + port := 3306 + if m[4] != "" { + if p, err := strconv.Atoi(m[4]); err == nil { + port = p + } + } + + name := strings.ReplaceAll(m[5], "%kernel.environment%", env) + + return &DBConfig{ + Host: m[3], + User: m[1], + Pass: m[2], + Name: name, + Port: port, + } +} + +// kernelEnv resolves the value Symfony substitutes for %kernel.environment%: +// a real APP_ENV environment variable wins, else the APP_ENV from the .env +// contents, else "prod" as a production-scan default. +func kernelEnv(fileData string) string { + if v := os.Getenv("APP_ENV"); v != "" { + return v + } + if m := symfonyAppEnvRgx.FindStringSubmatch(fileData); m != nil { + return m[1] + } + return "prod" +} + +// symfonyColumnURLs runs a single-column query and collects the non-empty +// results, the shared mechanics behind every Symfony platform's BaseURLs. +func symfonyColumnURLs(ctx context.Context, cfg *StoreConfig, query string) ([]string, error) { + db, err := ConnectDB(ctx, *cfg.DB) + if err != nil { + return nil, err + } + + rows, err := db.Query(query) + if err != nil { + return nil, err + } + + urls := []string{} + for rows.Next() { + var u string + if err := rows.Scan(&u); err == nil && u != "" { + urls = append(urls, u) + } + } + + if len(urls) == 0 { + return nil, errors.New("base url(s) not found in database") + } + return urls, nil +}