Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ Go config parsers for several PHP eCommerce platforms:
- Shopware 5 / 6
- OpenCart 4
- JTL-Shop
- Sylius
12 changes: 12 additions & 0 deletions fixture/sylius/.env
Original file line number Diff line number Diff line change
@@ -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 ###
12 changes: 12 additions & 0 deletions fixture/sylius/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions fixture/sylius/vendor/sylius/sylius/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "sylius/sylius",
"type": "library",
"description": "Modern e-commerce for Symfony."
}
7 changes: 7 additions & 0 deletions platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
60 changes: 3 additions & 57 deletions shopware6.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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%'`)
}
2 changes: 2 additions & 0 deletions shopware6_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
)

func TestConfigToDSN(t *testing.T) {
t.Setenv("DATABASE_URL", "")
sw6 := Shopware6{}
want := DBConfig{
Host: "localhost",
Expand All @@ -20,6 +21,7 @@ func TestConfigToDSN(t *testing.T) {
}

func TestConfigWithQuotesToDSN(t *testing.T) {
t.Setenv("DATABASE_URL", "")
sw6 := Shopware6{}
want := DBConfig{
Host: "localhost",
Expand Down
41 changes: 41 additions & 0 deletions sylius.go
Original file line number Diff line number Diff line change
@@ -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)
}
52 changes: 52 additions & 0 deletions sylius_config_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
115 changes: 115 additions & 0 deletions symfony.go
Original file line number Diff line number Diff line change
@@ -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_<SUFFIX> 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
}
Loading