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
49 changes: 30 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,26 @@ name: CI
on:
push:
pull_request:

jobs:
phpunit:
name: PHPUnit
ci:
name: CI
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
services:
mysql:
image: mysql:8.4
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: bga_test
options: >-
--health-cmd="mysqladmin ping"
Comment thread
Nodjo marked this conversation as resolved.
--health-interval=10s
--health-timeout=5s
--health-retries=3
ports:
- 3306:3306
steps:
- uses: actions/checkout@v4

Expand All @@ -18,26 +31,24 @@ jobs:
with:
php-version: '8.3'

- name: Install dependencies
run: composer install --no-progress --prefer-dist

- name: Run tests
run: ./vendor/bin/phpunit

format:
name: PHP CS Fixer
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
php-version: '8.3'
path: vendor
key: composer-${{ hashFiles('composer.lock') }}
restore-keys: composer-

- name: Install dependencies
run: composer install --no-progress --prefer-dist

- name: Check formatting
run: ./vendor/bin/php-cs-fixer fix --dry-run --diff

- name: Setup test database user
env:
MYSQL_PWD: root
run: |
mysql -h 127.0.0.1 -u root -e "CREATE USER 'bga_test'@'%' IDENTIFIED BY 'bga_test'; GRANT ALL PRIVILEGES ON bga_test.* TO 'bga_test'@'%'; FLUSH PRIVILEGES;"

- name: Run tests
run: ./vendor/bin/phpunit
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/.vscode/*
!/.vscode/settings.json
!/.vscode/extensions.json
!/.vscode/sftp.json.example

# Composer
/vendor/
Expand Down
30 changes: 30 additions & 0 deletions .vscode/sftp.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "BGA Studio",
"host": "1.studio.boardgamearena.com",
"protocol": "sftp",
"port": 2022,
"username": "${SFTP_USERNAME}",
"remotePath": "/diceforge",
"password": "${SFTP_PASSWORD}",
"uploadOnSave": true,
"ignore": [
".git",
".prettierignore",
".prettierrcold",
".vscode",
".gitattributes",
".gitignore",
"docs",
"rebellion.todo",
"tests",
"vendor",
"phpunit.xml.dist",
"phpunit.xml",
"phpunit.result.cache",
"composer.json",
"composer.lock",
"phpunit.xml",
"phpstan.neon",
"phpstan-custom-rules.neon"
]
}
42 changes: 42 additions & 0 deletions modules/php/Entities/Player.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Bga\Games\diceforge\Entities;

use Bga\Games\diceforge\Framework\Orm\Column;
use Bga\Games\diceforge\Framework\Orm\Entity;
use Bga\Games\diceforge\Framework\Orm\Id;

#[Entity('player')]
class Player
{
#[Id]
#[Column('player_id')]
public int $id = 0;

#[Column('player_score')]
public int $score = 0;

#[Column('player_name')]
public string $name;

#[Column('player_color')]
public string $color;

public function __construct(string $name, string $color)
{
$this->name = $name;
$this->color = $color;
}

/**
* Returns the player entry as expected by setupNewGame()'s $players array.
*/
public function toSetupArray(): array
{
return [
'player_canal' => '',
'player_name' => $this->name,
'player_avatar' => '',
];
}
}
32 changes: 32 additions & 0 deletions modules/php/Framework/Db/Db.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Bga\Games\diceforge\Framework\Db;

/**
* Thin wrapper over BGA's Table static database methods.
*
* Method names and signatures intentionally mirror Table's statics so that
* call sites read identically to the BGA docs and mocks are trivial to write.
*
* Why this exists: Table's methods are `final public static`, so they cannot
* be directly mocked or injected. This interface solves that without adding
* any translation layer.
*/
interface Db
{
public function DbQuery(string $sql): null|\mysqli_result|bool;

public function getUniqueValueFromDB(string $sql): mixed;

public function getCollectionFromDB(string $sql, bool $bSingleValue = false): array;

public function getObjectListFromDB(string $sql, bool $bUniqueValue = false): array;

public function getNonEmptyCollectionFromDB(string $sql): array;

public function getObjectFromDB(string $sql): array;

public function mysql_fetch_assoc(\mysqli_result $result): array|false|null;

public function escape(string $value): string;
Comment thread
Nodjo marked this conversation as resolved.
}
132 changes: 132 additions & 0 deletions modules/php/Framework/Db/MysqliDb.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

namespace Bga\Games\diceforge\Framework\Db;

/**
* mysqli-backed implementation of Db for local testing.
*
* Connects to a real MariaDB/MySQL server so all SQL — including
* mysql_fetch_assoc — works exactly as it does on the BGA platform.
*
* Run tests/setup-test-db.sh (once, with sudo) before using this class.
*
* Default connection targets the test database created by that script:
* host=127.0.0.1 user=bga_test password=bga_test db=bga_test
*/
class MysqliDb implements Db
{
public function __construct(private readonly \mysqli $mysqli)
{
}

public static function createForTest(
string $host = '127.0.0.1',
string $user = 'bga_test',
string $password = 'bga_test',
string $database = 'bga_test',
int $port = 3306,
): self {
$mysqli = new \mysqli($host, $user, $password, $database, $port);
if ($mysqli->connect_error) {
throw new \RuntimeException("MysqliDb: connection failed: " . $mysqli->connect_error);
}
$mysqli->set_charset('utf8');
return new self($mysqli);
}

public function getMysqli(): \mysqli
{
return $this->mysqli;
}

// -------------------------------------------------------------------------
// Interface implementation
// -------------------------------------------------------------------------

public function DbQuery(string $sql): null|\mysqli_result|bool
{
$result = $this->mysqli->query($sql);
if ($result === false) {
throw new \BgaSystemException("DbQuery failed: " . $this->mysqli->error . " | SQL: $sql");
}
return $result;
}

public function getUniqueValueFromDB(string $sql): mixed
{
$result = $this->DbQuery($sql);
if (!($result instanceof \mysqli_result)) {
return null;
}
$rows = $result->fetch_all(MYSQLI_NUM);
$result->free();
if (count($rows) > 1) {
throw new \BgaSystemException("getUniqueValueFromDB: query returned more than 1 row");
}
return $rows[0][0] ?? null;
}

public function getCollectionFromDB(string $sql, bool $bSingleValue = false): array
{
$result = $this->DbQuery($sql);
if (!($result instanceof \mysqli_result)) {
return [];
}
$rows = $result->fetch_all(MYSQLI_ASSOC);
$result->free();
$collection = [];
foreach ($rows as $row) {
$values = array_values($row);
$key = $values[0];
$collection[$key] = $bSingleValue ? ($values[1] ?? null) : $row;
}
return $collection;
}

public function getObjectListFromDB(string $sql, bool $bUniqueValue = false): array
{
$result = $this->DbQuery($sql);
if (!($result instanceof \mysqli_result)) {
return [];
}
$rows = $result->fetch_all(MYSQLI_ASSOC);
$result->free();
if (!$bUniqueValue) {
return $rows;
}
return array_map(fn (array $row) => reset($row), $rows);
}

public function getNonEmptyCollectionFromDB(string $sql): array
{
$result = $this->getCollectionFromDB($sql);
if (empty($result)) {
throw new \BgaSystemException("getNonEmptyCollectionFromDB: empty collection");
}
return $result;
}

public function getObjectFromDB(string $sql): array
{
$result = $this->DbQuery($sql);
if (!($result instanceof \mysqli_result)) {
return [];
}
$rows = $result->fetch_all(MYSQLI_ASSOC);
$result->free();
if (count($rows) > 1) {
throw new \BgaSystemException("getObjectFromDB: query returned more than 1 row");
}
return $rows[0] ?? [];
}

public function mysql_fetch_assoc(\mysqli_result $result): array|false|null
{
return $result->fetch_assoc();
}

public function escape(string $value): string
{
return $this->mysqli->real_escape_string($value);
}
}
Loading
Loading