Compare commits

..

4 commits

Author SHA1 Message Date
Joby Elliott
5fe55c2c0d Another null value fix 2019-03-07 09:07:05 -07:00
Joby Elliott
171e32fc18 cleaned up tests, fixed null value in arrays bug 2019-03-06 12:48:22 -07:00
Joby Elliott
0a2bbe44a1 updating config 2019-03-06 11:59:18 -07:00
Joby Elliott
7c4a51f95e legacy branch to support a certain troublesome old server 2019-03-06 11:27:37 -07:00
66 changed files with 2572 additions and 3096 deletions

View file

@ -1,46 +0,0 @@
name: Test suite
on: push
jobs:
phpunit:
name: PHPUnit
runs-on: ubuntu-latest
strategy:
matrix:
php: ["7.1", "7.2", "7.3", "7.4"]
services:
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: destructr_test
ports:
- 3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
mariadb:
image: mariadb:10.2
env:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: destructr_test
ports:
- 3306
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Composer install
run: composer install -o --no-progress --ignore-platform-reqs
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, mysql, mysqli, pdo_mysql
coverage: none
ini-values: variables_order=EGPCS
- name: PHPUnit
env:
TEST_MYSQL_SERVER: 127.0.0.1
TEST_MYSQL_PORT: ${{ job.services.mysql.ports['3306'] }}
TEST_MARIADB_SERVER: 127.0.0.1
TEST_MARIADB_PORT: ${{ job.services.mariadb.ports['3306'] }}
run: ./vendor/bin/phpunit

12
.gitignore vendored
View file

@ -1,9 +1,5 @@
vendor
composer.lock
.phpunit.result.cache
test.php
test.sqlite
*.test.sqlite
.DS_Store
examples/example.sqlite
examples/example.sqlite-journal
/vendor/
*.tmp
/test.php
*.ignore

8
.travis.yml Normal file
View file

@ -0,0 +1,8 @@
dist: precise
language: php
php:
- 5.3
- 5.4
- 5.5
- 5.6
install: composer install

137
README.md
View file

@ -1,123 +1,32 @@
# Destructr
# Digraph DataObject v0.5
[![PHPUnit Tests](https://github.com/jobyone/destructr/actions/workflows/test.yml/badge.svg)](https://github.com/jobyone/destructr/actions/workflows/test.yml)
[![Build Status](https://travis-ci.org/digraphcms/digraph-dataobject.svg?branch=v0.5)](https://travis-ci.org/digraphcms/digraph-dataobject)
Destructr is a specialized ORM that allows a seamless mix of structured, relational data with unstructured JSON data.
*This is not going to be the final version of Digraph DataObjects.*
I will be supporting the v0.5 branch with its current interface, at least for myself, because I am using it in a few production projects. Those projects are stuck on some pretty weird old custom builds of PHP, and so this branch will be maintaining PHP 5.3 compatibility for the foreseeable future. If you're on a more recent version than PHP 5.3 (as you very much should be), you shouldn't use this branch. You should instead wait for the 1.0 release, which should hopefully be in 2018 sometime. The 1.0 branch will fix a lot of conceptual shortcomings of the alpha versions, and is designed to target modern environments and take full advantage of all the features of PHP 7.1 that make it finally act something like a grown-up programming language.
## Getting started
So I guess in a nutshell: This version is relatively stable, and the author is actually using it in production. The interface shouldn't change significantly, unless I find something absolutely terrible. You still probably shouldn't use it, though, because it's targeting a truly decrepit version of PHP and better things are on the way.
The purpose of Destructr is to allow many "types" of objects to be stored in a single table.
Every Destructr Data Object (DSO) simply contains an array of data that will be saved into the database as JSON.
Array access is also flattened, using dots as delimiters, so rather than reading `$dso["foo"]["bar"]` you access that data via `$dso["foo.bar"]`.
This is for two reasons.
It sidesteps the issue of updating nested array values by reference, and it creates an unambiguous way of locating a node of the unstructured data with a string, which mirrors how we can write SQL queries to reference them.
Also this version will probably never be properly documented.
If this sounds like an insanely slow idea, that's because it is.
Luckily MySQL and MariaDB have mechanisms we can take advantage of to make generated columns from any part of the unstructured data, so that pieces of it can be pulled out into their own virtual columns for indexing and faster searching/sorting.
## MIT License
### Database driver and factory
Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
In order to read/write objects from a database table, you'll need to configure a Driver and Factory class.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
```php
// DriverFactory::factory() has the same arguments as PDO::__construct
// You can also construct a driver directly, from a class in Drivers,
// but for common databases DriverFactory::factory should pick the right class
$driver = \Destructr\DriverFactory::factory(
'mysql:host=127.0.0.1',
'username',
'password'
);
// Driver is then used to construct a Factory
$factory = new \Destructr\Factory(
$driver, //driver is used to manage connection and generate queries
'dso_objects' //all of a Factory's data is stored in a single table
);
```
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
### Creating a new record
Next, you can use the factory to create a new record object.
```php
// by default all objects are the DSO class, but factories can be made to use
// other classes depending on what data objects are instantiated with
$obj = $factory->create();
// returns boolean indicating whether insertion succeeded
// insert() must be called before update() will work
$obj->insert();
// set a new value and call update() to save changes into database. update()
// will return true without doing anything if no changes have been made.
$obj['foo.bar'] = 'some value';
$obj->update();
// deleting an object will by default just set dso.deleted to the current time
// objects with a non-null dso.deleted are excluded from queries by default
// delete() calls update() inside it, so its effect is immediate
$obj->delete();
// objects that were deleted via default delete() are recoverable via undelete()
// undelete() also calls update() for you
$obj->undelete();
// objects can be actually removed from the table by calling delete(true)
$obj->delete(true);
```
### Searching
Factories provide an interface for creating `Search` objects, which allow you to enter in various SQL clauses in a structured and abstract fashion.
```php
// get a new search object from the factory
$search = $factory->search();
// Search::where() takes SQL for the WHERE clause of a query
// ${path} syntax is used to reference data within objects, and
// works everywhere in searches
$search->where('${dso.date.modified} > :time');
// Search::order() takes SQL to go inside an ORDER BY clause
// in the final query.
$search->order('${dso.date.modified} desc');
// Search limit/offset methods can be used for pagination
// there is also a paginate() method for more conveniently
// paginating results
$search->paginate(20,1);
// Search::execute() returns an array of the resulting objects
$results = $search->execute();
```
## Requirements
This system relies **heavily** on the JSON features of the underlying database.
This means it cannot possibly run without a database that supports JSON features.
Basically if a database doesn't have JSON functions it's probably impossible for Destructr to ever work with it.
At the moment there is pretty decent support for:
* MySQL >=5.7.8
* MariaDB >=10.2.7
* SQLite 3 (with some caveats)
In practice this means Destructr will **never** be able to run on less than the following versions of the following popular databases:
* MySQL >=5.7.8
* MariaDB >=10.2.7
* PostgreSQL >=9.3
* SQL Server >=2016
Theoretically Destructr is also an excellent fit for NoSQL databases.
If I ever find myself needing it there's a good chance it's possible to write drivers for running it on something like MongoDB as well.
It might even be kind of easy.
### SQLite caveats
MySQL and MariaDB drivers set virtual columns to be generated automatically using their native JSON functions.
SQLite doesn't have native JSON (in most environments, at least), so Destructr itself manually updates virtual columns whenever objects are inserted or updated.
In practice this won't matter *if* you are doing all your insertion and updating via Destructr.
If you're doing updates to your database via any other method, however, you need to be aware of this, and manually update the virtual column values.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,40 +1,33 @@
{
"name": "byjoby/destructr",
"description": "A library for storing a mix of structured and unstructured data in relational databases",
"description": "Interfaces and abstract classes for managing CRUD through straightforward PHP classes. Primarily focused on creating objects that yield clean readable code where they are used.",
"type": "library",
"license": "MIT",
"minimum-stability": "dev",
"prefer-stable": true,
"authors": [{
"name": "Joby Elliott",
"email": "joby@byjoby.com"
}],
"require": {
"php": ">=7.1",
"byjoby/flatrr": "^1"
},
"require-dev": {
"phpunit/phpunit": "^7",
"phpunit/dbunit": "^4"
},
"scripts": {
"test": [
"phpunit"
],
"test-mysql": [
"phpunit --testsuite MySQL"
],
"test-mariadb": [
"phpunit --testsuite MariaDB"
],
"test-sqlite": [
"phpunit --testsuite SQLite"
]
"php": ">=5.3.3",
"fpdo/fluentpdo": "^1.1"
},
"autoload": {
"psr-4": {
"Destructr\\": "src/"
"Digraph\\DataObject\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Destructr\\": "tests/"
"Digraph\\DataObject\\Tests\\": "tests/"
}
},
"require-dev": {
"phpunit/phpunit": "^4.8",
"phpunit/dbunit": "^1.4"
},
"scripts": {
"test": [
"phpunit"
]
}
}

View file

@ -1,34 +0,0 @@
<?php
use Destructr\Factory;
class ExampleFactory extends Factory
{
/**
* Example factory with a different schema, to index on random_data for faster searching
*/
protected $schema = [
'dso.id' => [
'name' => 'dso_id', //column name to be used
'type' => 'VARCHAR(16)', //column type
'index' => 'BTREE', //whether/how to index
'unique' => true, //whether column should be unique
'primary' => true, //whether column should be the primary key
],
'dso.type' => [
'name' => 'dso_type',
'type' => 'VARCHAR(30)',
'index' => 'BTREE',
],
'dso.deleted' => [
'name' => 'dso_deleted',
'type' => 'BIGINT',
'index' => 'BTREE',
],
'random_data' => [
'name' => 'random_data',
'type' => 'VARCHAR(64)',
'index' => 'BTREE',
],
];
}

View file

@ -1,50 +0,0 @@
<?php
/**
* This file demonstrates some basic uses of Destructr, from the creation
* of a connection and factory, through to creating, inserting, updating,
* deleting, and querying data.
*/
include __DIR__ . '/../vendor/autoload.php';
/*
SQLite drivers can be created by the default factory.
A charset of UTF8 should be specified, to avoid character encoding
issues.
*/
$driver = \Destructr\DriverFactory::factory(
'sqlite:' . __DIR__ . '/example.sqlite'
);
/*
Creates a factory using the table 'example_table', and creates
the necessary table. Note that prepareEnvironment() can safely be called
multiple times. updateEnvironment shouldn't be used this way in production,
as if it is called more than once per second during a schema change, errors
may be introduced.
*/
include __DIR__ . '/example_factory.php';
$factory = new ExampleFactory($driver, 'example_table');
$factory->prepareEnvironment();
$factory->updateEnvironment();
/*
Inserting a record
*/
$obj = $factory->create(
[
'dso.type'=>'foobar',
'random_data' => md5(rand())
]
);
$obj->insert();
/*
Search by random data field, which is indexed due to the
ExampleFactory class' $schema property.
*/
$search = $factory->search();
$search->where('${random_data} LIKE :q');
$result = $search->execute(['q'=>'ab%']);
foreach($result as $r) {
var_dump($r->get());
}

View file

@ -1,13 +1,7 @@
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="MySQL">
<directory>tests/Drivers/MySQL</directory>
</testsuite>
<testsuite name="MariaDB">
<directory>tests/Drivers/MariaDB</directory>
</testsuite>
<testsuite name="SQLite">
<directory>tests/Drivers/SQLite</directory>
<testsuite name="Digraph Tests">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>

View file

@ -0,0 +1,77 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject;
abstract class AbstractArrayDataObject extends AbstractDataObject implements \ArrayAccess, \Iterator
{
protected $iterMap = array();
protected $iterPos = 0;
protected function buildIterMap()
{
$this->iterMap = array();
foreach ($this->map() as $key => $value) {
$this->iterMap[] = $key;
}
}
public function offsetSet($offset, $value)
{
$this->$offset = $value;
$this->buildIterMap();
}
public function offsetExists($offset)
{
return isset($this->$offset);
}
public function offsetUnset($offset)
{
throw new Exceptions\Exception("Can't unset a DataObject field", 1);
}
public function offsetGet($offset)
{
$return = $this->$offset;
return $return;
}
public function rewind()
{
$this->iterPos = 0;
}
public function &current()
{
$key = $this->key();
if (isset($this->$key)) {
$return = $this->$key;
return $return;
}
return null;
}
public function key()
{
return isset($this->iterMap[$this->iterPos]) ? $this->iterMap[$this->iterPos] : null;
}
public function next()
{
$this->iterPos++;
}
public function valid()
{
$key = $this->key();
return isset($this->$key);
}
}

549
src/AbstractDataObject.php Normal file
View file

@ -0,0 +1,549 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject;
use \Digraph\DataObject\Files\FilesContainer;
use \Digraph\DataObject\JSON\JSONContainer;
abstract class AbstractDataObject implements DataObjectInterface
{
/**
* Characters that are allowed in ID generation
* @var string
*/
protected static $_idChars = 'abcdefghijklmnopqrstuvwxyz0123456789';
/**
* Length of random portion of ID -- with the default 36 character set, 12
* characters gives you about 62 bits of info, which is probably good for
* most applications, even after profanity filtering
* @var int
*/
protected static $_idLength = 12;
/**
* Prefix for this class's objects
* @var string
*/
protected static $_idPrefix = 'do-';
/**
* Prefix for this class's storage names
* @var string
*/
protected static $_storagePrefix = '';
/**
* Stores the current data for this object
* @var array
*/
protected $_data = array();
/**
* Stores a list of what data has been changed in this object
* @var array
*/
protected $_dataChanged = array();
/**
* Stores the rules for transforming properties as they are get/set
* @var array
*/
protected $_transforms = array();
/**
* Holds static transforms that are loaded for each class at construction
* @var array
*/
protected static $_classTransforms = array(
'JSON' => array(
'class' => '\\Digraph\\DataObject\\DataTransformers\\JSON'
),
'bool' => array(
'set' => '\\Digraph\\DataObject\\DataTransformers\\SimpleTransforms::bool_set',
'get' => '\\Digraph\\DataObject\\DataTransformers\\SimpleTransforms::bool_get'
)
);
/**
* Holds transformation objects for properties that have them defined via
* class transforms and the map
* @var array
*/
protected $_transformObjects = array();
/**
* Construct a new copy of a DataObject -- data values can be specified,
* and those with defaults will be populated and those with generators will
* be generated (overriding anything the user requests)
* @param array $data
*/
public function __construct($data = array(), $fromRaw = false)
{
$this->registerClassTransforms();
//set up class transformers
foreach ($this->map() as $name => $map) {
if (isset($map['transform'])) {
if (isset($this->_transforms[$map['transform']]['class'])) {
$class = $this->_transforms[$map['transform']]['class'];
$this->_transformObjects[$name] = new $class($this);
}
}
}
//set values
if (!$fromRaw) {
$data = $this->dataGenerate($data, true);
foreach ($data as $name => $value) {
$this->set($name, $value);
}
} else {
foreach ($data as $name => $value) {
$this->setRaw($name, $value);
}
}
//build itermap
$this->buildIterMap();
}
/**
* getter for retrieving all changed data
* @return array
*/
public function _get_dataChanged()
{
foreach ($this->_transformObjects as $name => $object) {
$this->_dataChanged[$name] = true;
}
$changed = array();
foreach ($this->_dataChanged as $name => $value) {
$changed[] = $name;
}
return $changed;
}
/**
* getter for the raw storage values of all data
* @return array
*/
public function _get_dataRaw()
{
$this->dataChanged;
return $this->_data;
}
/**
* walk up the inheritance list, registering all class transforms
* @return void
*/
protected function registerClassTransforms($class = false)
{
if (!$class) {
$class = get_called_class();
}
$parent = false;
if ($parent = get_parent_class($class)) {
$parent::registerClassTransforms($parent);
}
foreach ($class::$_classTransforms as $key => $value) {
$this->_transforms[$key] = $value;
}
}
/**
* Given an array of name/value pairs, fill it out with any defaults or
* anything that must be generated per the map
* @param array $data
* @return array
*/
public function dataGenerate($data = array())
{
$dataOut = array();
//set values of $dataOut from $data or defaults and queue things that need generation
foreach (static::map() as $name => $conf) {
if (isset($data[$name])) {
//simple value being set in data
$dataOut[$name] = $data[$name];
} elseif (isset($conf['generated'])) {
//generated value
$dataOut[$name] = $this->generateValue($name);
} elseif (isset($conf['default'])) {
//default value
$dataOut[$name] = $conf['default'];
} else {
//null is the default of defaults
$dataOut[$name] = null;
}
}
//return
return $dataOut;
}
/**
* generate a value for a particular parameter name
* @param string $name
* @return mixed
*/
public function generateValue($name)
{
$map = $this->mapEntry($name);
$generator = $map['generated'];
if (method_exists($this, $generator)) {
return $this->$generator($name);
}
if (is_callable($generator)) {
return $generator($name);
}
throw new Exceptions\Exception("No valid generator found for $name", 1);
}
/**
* Get a value using its custom getter
* @param string $name
* @return mixed
*/
protected function get($name)
{
$map = $this->mapEntry($name);
//getters by property name
$getter = '_get_'.$name;
if (method_exists($this, $getter)) {
return $this->$getter($name);
}
//transformed
if (isset($map['transform']) && isset($this->_transforms[$map['transform']])) {
$transform = $this->_transforms[$map['transform']];
//transformation object
if (isset($transform['class'])) {
return $this->ref($this->_transformObjects[$name]);
}
//transformation function
if (isset($transform['get'])) {
$getter = $transform['get'];
if (method_exists($this, $getter)) {
return $this->$getter($name);
}
}
}
//just regular return
if (array_key_exists($name, $this->_data)) {
$return = $this->getRaw($name);
return $return;
}
$return = null;
return $return;
}
protected function &ref(&$var)
{
return $var;
}
/**
* Set the value of a field using its custom setters
* @param string $name
* @param mixed $value
*/
protected function set($name, $value)
{
$map = $this->mapEntry($name);
//setters by property name
$setter = '_set_'.$name;
if (method_exists($this, $setter)) {
$this->$setter($name, $value);
return $this->get($name);
}
//transformed
if (isset($map['transform']) && isset($this->_transforms[$map['transform']])) {
$transform = $this->_transforms[$map['transform']];
//transformation object
if (isset($transform['class'])) {
if ($this->_transformObjects[$name]->getUserValue() != $value) {
$this->_dataChanged[$name] = true;
}
$this->_transformObjects[$name]->setUserValue($value);
return $this->get($name);
}
//transformation function
$transform = $this->_transforms[$map['transform']];
if (isset($transform['set'])) {
$setter = $transform['set'];
if (method_exists($this, $setter)) {
if ($value != $this->get($name)) {
$this->_dataChanged[$name] = true;
}
$this->$setter($name, $value);
return $this->get($name);
}
}
}
//just regular value
if (array_key_exists($name, $this->map())) {
if ($value != $this->get($name)) {
$this->_dataChanged[$name] = true;
}
$this->setRaw($name, $value);
return $this->get($name);
}
}
/**
* @param string $name
* @return mixed
*/
public function __get($name)
{
$mapEntry = $this->mapEntry($name);
//hide masked values
if (isset($mapEntry['masked']) && $mapEntry['masked']) {
$return = null;
return $return;
}
//use internal getter
return $this->get($name);
}
/**
* @param string $name
* @param mixed $value
* @return mixed
*/
public function __set($name, $value)
{
//load map entry
$mapEntry = $this->mapEntry($name);
//check for not setting masked values
if (isset($mapEntry['masked']) && $mapEntry['masked']) {
return null;
}
//check for not setting system values
if (isset($mapEntry['system']) && $mapEntry['system']) {
throw new Exceptions\Exception("Public interface doesn't allow setting system values (tried to set $name)", 1);
}
//do the actual setting
return $this->set($name, $value);
}
/**
* @param string $name
* @return boolean
*/
public function __isset($name)
{
//load map entry
$mapEntry = $this->mapEntry($name);
//hide masked values
if (isset($mapEntry['masked']) && $mapEntry['masked']) {
return false;
}
//function name getters
$getter = '_get_' . $name;
if (method_exists($this, $getter)) {
return true;
}
//map names
if (array_key_exists($name, $this->map())) {
return true;
}
return false;
}
/**
* get the raw value of an item
* @param string $name
* @return mixed
*/
protected function getRaw($name)
{
if (!$this->mapEntry($name)) {
throw new Exceptions\Exception("\"$name\" not mapped for \"".get_called_class()."\"", 1);
}
if (isset($this->_transformObjects[$name])) {
$this->_data[$name] = $this->_transformObjects[$name]->getStorageValue();
}
if (!isset($this->_data[$name])) {
return null;
}
return $this->_data[$name];
}
/**
* set the raw value of an item
* @param string $name
* @param mixed $value
*/
protected function setRaw($name, $value)
{
if (!array_key_exists($name, $this->map())) {
throw new Exceptions\Exception("\"$name\" not mapped for \"".get_called_class()."\"", 1);
}
//transform object
if (isset($this->_transformObjects[$name])) {
$this->_transformObjects[$name]->setStorageValue($value);
$value = $this->_transformObjects[$name]->getStorageValue();
if (!isset($this->_data[$name])) {
$this->_data[$name] = $value;
}
return $value;
}
//regular value
$this->_data[$name] = $value;
return $this->_data[$name] = $value;
}
/**
* Return the same data as getMap, but with $_storagePrefix applied to each
* entry's name data.
* @return Array
*/
protected static $_maps = array();
public static function map()
{
$class = get_called_class();
if (!isset(static::$_maps[$class])) {
$map = static::getMap();
if (static::$_storagePrefix) {
foreach ($map as $key => $value) {
if (strpos($map[$key]['name'], 'do_') === 0) {
$map[$key]['name'] = preg_replace('/^do_/', static::$_storagePrefix, $value['name']);
} else {
$map[$key]['name'] = static::$_storagePrefix.$map[$key]['name'];
}
}
}
static::$_maps[$class] = $map;
}
return static::$_maps[$class];
}
/**
* Return a map of this class's data names and how they are named in the
* underlying storage layer. Abstract classes provide the bare minimum
* mapping to create a DataObject, so child classes should extend what is
* returned by parent::getMap()
* @return Array
*/
public static function getMap()
{
return array(
'do_id' => array(
'name' => 'do_id',
'system' => true,
'generated' => 'generateID'
),
'do_cdate' => array(
'name' => 'do_cdate',
'system' => true,
'generated' => 'time'
),
'do_cuser' => array(
'name' => 'do_cuser',
'system' => true,
'generated' => 'generateUser',
'transform' => 'JSON'
),
'do_mdate' => array(
'name' => 'do_mdate',
'system' => true,
'generated' => 'time'
),
'do_muser' => array(
'name' => 'do_muser',
'system' => true,
'generated' => 'generateUser',
'transform' => 'JSON'
),
'do_deleted' => array(
'name' => 'do_deleted',
'system' => true
)
);
}
/**
* Retrieve a map entry by name
* @param string $name
* @return array
*/
public static function mapEntry($name)
{
$class = get_called_class();
if (!isset(static::$_maps[$class])) {
static::map();
}
if (isset(static::$_maps[$class][$name])) {
return static::$_maps[$class][$name];
}
return false;
}
/**
* Retrieve the storage name of a map entry
* @param string $name
* @return string
*/
public static function storageName($name)
{
$mapEntry = static::mapEntry($name);
if (!$mapEntry) {
return false;
}
return $mapEntry['name'];
}
/**
* generate a new ID
* @param string $name
* @param array $data
* @return string
*/
public static function generateID($name, $depth = 0)
{
if ($depth >= 10) {
throw new Exceptions\IDSpaceException();
}
$id = '';
while (strlen($id) < static::$_idLength) {
$id .= substr(
static::$_idChars,
rand(0, strlen(static::$_idChars)-1),
1
);
}
$id = static::$_idPrefix.$id;
if (Utils\BadWords::profane($id) || static::idExists($id)) {
return static::generateID($name, $depth+1);
}
return $id;
}
/**
* Generate an array holding information about the current user
* @param string $name
* @param array $data
* @return array
*/
public static function generateUser($name)
{
$out = array();
$out['ip'] = $_SERVER['REMOTE_ADDR'];
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$out['fw'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
return $out;
}
}

View file

@ -1,120 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr;
use \Flatrr\FlatArray;
/**
* Interface for DeStructured Objects (DSOs). These are the class that is
* actually used for storing and retrieving partially-structured data from the
* database.
*/
class DSO extends FlatArray implements DSOInterface
{
protected $factory;
protected $changes;
protected $removals;
public function __construct(array $data = null, Factory $factory = null)
{
$this->resetChanges();
parent::__construct($data);
$this->factory($factory);
$this->resetChanges();
}
public function hook_create()
{
//does nothing
}
public function hook_update()
{
//does nothing
}
public function delete(bool $permanent = false): bool
{
return $this->factory->delete($this, $permanent);
}
public function undelete(): bool
{
return $this->factory->undelete($this);
}
public function insert(): bool
{
return $this->factory()->insert($this);
}
public function update(bool $sneaky = false): bool
{
return $this->factory()->update($this);
}
public function resetChanges()
{
$this->changes = new FlatArray();
$this->removals = new FlatArray();
}
public function changes(): array
{
return $this->changes->get();
}
public function removals(): array
{
return $this->removals->get();
}
public function set(?string $name, $value, $force = false)
{
$name = strtolower($name);
if ($this->get($name) === $value) {
return;
}
if (is_array($value)) {
//check for what's being removed
if (is_array($this->get($name))) {
foreach ($this->get($name) as $k => $v) {
if (!isset($value[$k])) {
if ($name) {
$k = $name . '.' . $k;
}
$this->unset($k);
}
}
} else {
parent::set($name, []);
}
//recursively set individual values so we can track them
foreach ($value as $k => $v) {
if ($name) {
$k = $name . '.' . $k;
}
$this->set($k, $v, $force);
}
} else {
$this->changes->set($name, $value);
unset($this->removals[$name]);
parent::set($name, $value);
}
}
function unset(?string $name) {
if (isset($this[$name])) {
$this->removals->set($name, $this->get($name));
unset($this->changes[$name]);
parent::unset($name);
}
}
public function factory(Factory $factory = null): ?Factory
{
if ($factory) {
$this->factory = $factory;
}
return $this->factory;
}
}

View file

@ -1,27 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr;
use Flatrr\FlatArrayInterface;
/**
* Interface for DeStructure Objects (DSOs). These are the class that is
* actually used for storing and retrieving partially-structured data from the
* database.
*/
interface DSOInterface extends FlatArrayInterface
{
public function __construct(array $data = null, Factory $factory = null);
public function factory(Factory $factory = null): ?Factory;
public function set(?string $name, $value, $force = false);
public function resetChanges();
public function changes(): array;
public function removals(): array;
public function insert(): bool;
public function update(bool $sneaky = false): bool;
public function delete(bool $permanent = false): bool;
public function undelete(): bool;
}

View file

@ -0,0 +1,40 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject;
interface DataObjectInterface
{
public function create();
public static function read($id);
public static function search($parameters = array(), $sort = array(), $options = array());
public function update($action = null);
public function delete($permanent = false);
public static function idExists($id);
public function __construct($data = array(), $fromRaw = false);
public static function map();
public static function getMap();
public static function mapEntry($name);
public static function storageName($name);
public function __get($name);
public function __set($name, $value);
public function __isset($name);
}

View file

@ -0,0 +1,49 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\DataTransformers;
abstract class AbstractDataTransformer implements DataTransformerInterface
{
protected $parent = null;
protected $changed = false;
public function __construct(&$parent)
{
$this->setParent($parent);
}
public function setParent(&$parent)
{
$this->parent = $parent;
}
public function &getParent()
{
return $this->parent;
}
public function changed($changed=true)
{
$this->changed = $changed;
}
public function isChanged()
{
return $this->changed;
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\DataTransformers;
interface DataTransformerInterface
{
public function __construct(&$parent);
public function setParent(&$parent);
public function &getParent();
public function setStorageValue($storageValue);
public function getStorageValue();
public function setUserValue($userValue);
public function getUserValue();
public function changed($changed=true);
public function isChanged();
}

View file

@ -0,0 +1,181 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\DataTransformers;
use \Digraph\DataObject\DataTransformers\AbstractDataTransformer;
class JSON extends AbstractDataTransformer implements \ArrayAccess, \Iterator
{
private $arrayAccessData = array();
private $iteratorArrayMap = array();
private $jsonParent = null;
public function __construct(&$parent, &$jsonParent=null)
{
$this->setParent($parent, $jsonParent);
}
public function changed($changed = true)
{
parent::changed($changed);
if ($this->jsonParent) {
$this->jsonParent->changed($changed);
}
}
public function setParent(&$parent, &$jsonParent=null)
{
parent::setParent($parent);
$this->jsonParent = $jsonParent;
}
public function fieldCount()
{
return count($this->arrayAccessData);
}
public function setUserValue($userValue)
{
if (is_array($userValue)) {
foreach ($userValue as $key => $value) {
if ($value !== null) {
$this[$key] = $value;
}
}
}
}
public function setStorageValue($storageValue)
{
$this->setUserValue(json_decode($storageValue, true));
}
public function getStorageValue()
{
return json_encode($this->toArray());
}
public function toArray()
{
return $this->getUserValue();
}
public function getUserValue()
{
$array = array();
foreach ($this as $key => $value) {
if ($value instanceof JSON) {
$value = $value->toArray();
}
$array[$key] = $value;
}
return $array;
}
private function buildIterMap()
{
$this->iteratorArrayMap = array();
foreach ($this->arrayAccessData as $key => $value) {
$this->iteratorArrayMap[] = $key;
}
}
public function offsetSet($offset, $value)
{
//do nothing for null values
if ($value === null) {
return;
}
//set offset
if (is_null($offset)) {
$this->arrayAccessData[] = $value;
$offset = count($this->arrayAccessData);
}
//array values convert to JSON
if (is_array($value)) {
$array = $value;
$value = new JSON($this->getParent(), $this);
$value->setUserValue($array);
}
//JSON values need a parent set
if ($value instanceof JSON) {
$value->setParent($this->getParent(), $this);
}
//set in arrayAccessData
if (isset($this->arrayAccessData[$offset])) {
if ($this->arrayAccessData[$offset] != $value) {
$this->changed();
}
}
$this->arrayAccessData[$offset] = $value;
$this->buildIterMap();
}
public function offsetExists($offset)
{
return isset($this->arrayAccessData[$offset]);
}
public function offsetUnset($offset)
{
unset($this->arrayAccessData[$offset]);
$this->buildIterMap();
}
protected function &getRef($offset)
{
return $this->arrayAccessData[$offset];
}
public function offsetGet($offset)
{
if (isset($this->arrayAccessData[$offset])) {
return $this->getRef($offset);
}
return null;
}
public function rewind()
{
$this->iterPos = 0;
}
public function &current()
{
if (isset($this->arrayAccessData[$this->key()])) {
return $this->arrayAccessData[$this->key()];
}
$return = null;
return $return;
}
public function key()
{
return isset($this->iteratorArrayMap[$this->iterPos]) ? $this->iteratorArrayMap[$this->iterPos] : null;
}
public function next()
{
$this->iterPos++;
}
public function valid()
{
return isset($this->arrayAccessData[$this->key()]);
}
}

View file

@ -0,0 +1,23 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\DataTransformers;
class SimpleTransforms implements DataTransformerInterface
{
}

View file

@ -1,40 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr;
class DriverFactory
{
public static $map = [
'mariadb' => Drivers\MariaDBDriver::class,
'mysql' => Drivers\MySQLDriver::class,
'sqlite' => Drivers\SQLiteDriver::class,
];
public static function factory(string $dsn, string $username = null, string $password = null, array $options = null, string $type = null): ?Drivers\AbstractDriver
{
if (!$type) {
$type = @array_shift(explode(':', $dsn, 2));
}
$type = strtolower($type);
if ($class = @static::$map[$type]) {
return new $class($dsn, $username, $password, $options);
} else {
return null;
}
}
public static function factoryFromPDO(\PDO $pdo, string $type = null): ?Drivers\AbstractDriver
{
if (!$type) {
$type = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
}
$type = strtolower($type);
if ($class = @static::$map[$type]) {
$f = new $class();
$f->pdo($pdo);
return $f;
} else {
return null;
}
}
}

View file

@ -1,24 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr\Drivers;
use Destructr\DSOInterface;
use Destructr\Search;
abstract class AbstractDriver
{
const SCHEMA_TABLE = 'destructr_schema';
abstract public function errorInfo();
abstract public function update(string $table, DSOInterface $dso): bool;
abstract public function delete(string $table, DSOInterface $dso): bool;
abstract public function count(string $table, Search $search, array $params): int;
abstract public function select(string $table, Search $search, array $params);
abstract public function insert(string $table, DSOInterface $dso): bool;
abstract public function beginTransaction(): bool;
abstract public function commit(): bool;
abstract public function rollBack(): bool;
abstract public function prepareEnvironment(string $table, array $schema): bool;
abstract public function updateEnvironment(string $table, array $schema): bool;
abstract public function checkEnvironment(string $table, array $schema): bool;
}

View file

@ -1,384 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr\Drivers;
use Destructr\DSOInterface;
use Destructr\Search;
use PDO;
abstract class AbstractSQLDriver extends AbstractDriver
{
public $lastPreparationErrorOn;
public $pdo;
protected $schemas = [];
protected $transactionsEnabled = true;
abstract protected function sql_ddl(array $args = []): string;
abstract protected function expandPath(string $path): string;
abstract protected function sql_set_json(array $args): string;
abstract protected function sql_insert(array $args): string;
abstract protected function sql_create_schema_table(): string;
abstract protected function sql_table_exists(string $table): string;
abstract protected function buildIndexes(string $table, array $schema): bool;
abstract protected function addColumns($table, $schema): bool;
abstract protected function removeColumns($table, $schema): bool;
abstract protected function rebuildSchema($table, $schema): bool;
public function __construct(string $dsn = null, string $username = null, string $password = null, array $options = null)
{
if ($dsn) {
if (!($pdo = new \PDO($dsn, $username, $password, $options))) {
throw new \Exception("Error creating PDO connection");
}
$this->pdo($pdo);
}
}
public function tableExists(string $table): bool
{
try {
$stmt = $this->pdo()->prepare($this->sql_table_exists($table));
if ($stmt && $stmt->execute() !== false) {
return true;
} else {
return false;
}
} catch (\Throwable $th) {
return false;
}
}
public function createSchemaTable()
{
try {
$this->pdo->exec($this->sql_create_schema_table());
} catch (\Throwable $th) {
}
return $this->tableExists(AbstractDriver::SCHEMA_TABLE);
}
public function pdo(\PDO $pdo = null): ?\PDO
{
if ($pdo) {
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$this->pdo = $pdo;
}
return $this->pdo;
}
public function disableTransactions()
{
$this->transactionsEnabled = false;
}
public function enableTransactions()
{
$this->transactionsEnabled = true;
}
public function beginTransaction(): bool
{
if (!$this->transactionsEnabled) return true;
return $this->pdo->beginTransaction();
}
public function commit(): bool
{
if (!$this->transactionsEnabled) return true;
return $this->pdo->commit();
}
public function rollBack(): bool
{
if (!$this->transactionsEnabled) return true;
return $this->pdo->rollBack();
}
protected function expandPaths($value)
{
if ($value === null) {
return null;
}
$value = preg_replace_callback(
'/\$\{([^\}\\\]+)\}/',
function ($matches) {
return $this->expandPath($matches[1]);
},
$value
);
return $value;
}
public function errorInfo()
{
return $this->pdo->errorInfo();
}
public function prepareEnvironment(string $table, array $schema): bool
{
$this->beginTransaction();
if ($this->createSchemaTable() && $this->createTable($table, $schema)) {
$this->commit();
return true;
} else {
$this->rollBack();
return false;
}
}
public function updateEnvironment(string $table, array $schema): bool
{
$this->beginTransaction();
if ($this->updateTable($table, $schema)) {
$this->commit();
return true;
} else {
$this->rollBack();
return false;
}
}
public function checkEnvironment(string $table, array $schema): bool
{
return $this->tableExists(AbstractDriver::SCHEMA_TABLE) && $this->getSchema($table) == $schema;
}
protected function updateTable($table, $schema): bool
{
$current = $this->getSchema($table);
$new = $schema;
if (!$current || $schema == $current) {
return true;
}
//do nothing with totally unchanged columns
foreach ($current as $c_id => $c) {
foreach ($schema as $n_id => $n) {
if ($n == $c && $n_id == $c_id) {
unset($current[$c_id]);
unset($new[$n_id]);
}
}
}
$removed = $current;
$added = $new;
//apply changes
$out = [
'removeColumns' => $this->removeColumns($table, $removed),
'addColumns' => $this->addColumns($table, $added),
'rebuildSchema' => $this->rebuildSchema($table, $schema),
'buildIndexes' => $this->buildIndexes($table, $schema),
'saveSchema' => $this->saveSchema($table, $schema),
];
foreach ($out as $k => $v) {
if (!$v) {
user_error("An error occurred during updateTable for $table. The error happened during $k.", E_USER_WARNING);
}
}
return !!array_filter($out);
}
public function createTable(string $table, array $schema): bool
{
// check if table exists, if it doesn't, save into schema table
$tableExists = $this->tableExists($table);
// if table exists, but no schema does, assume table matches schema and save schema
if ($tableExists && !$this->getSchema($table)) {
$this->saveSchema($table, $schema);
return true;
}
// create table from scratch
$sql = $this->sql_ddl([
'table' => $table,
'schema' => $schema,
]);
$out = $this->pdo->exec($sql) !== false;
if ($out) {
$this->buildIndexes($table, $schema);
if (!$tableExists) {
$this->saveSchema($table, $schema);
}
}
return $out;
}
public function getSchema(string $table): ?array
{
if (!isset($this->schemas[$table])) {
$s = $this->getStatement(
'get_schema',
['table' => $table]
);
if (!$s->execute(['table' => $table])) {
$this->schemas[$table] = null;
} else {
if ($row = $s->fetch(\PDO::FETCH_ASSOC)) {
$this->schemas[$table] = @json_decode($row['schema_schema'], true);
} else {
$this->schemas[$table] = null;
}
}
}
return @$this->schemas[$table];
}
public function saveSchema(string $table, array $schema): bool
{
$out = $this->pdo->exec(
$this->sql_save_schema($table, $schema)
) !== false;
unset($this->schemas[$table]);
return $out;
}
protected function sql_save_schema(string $table, array $schema)
{
$time = time();
$table = $this->pdo->quote($table);
$schema = $this->pdo->quote(json_encode($schema));
return <<<EOT
INSERT INTO `destructr_schema`
(schema_time,schema_table,schema_schema)
VALUES ($time,$table,$schema);
EOT;
}
protected function sql_get_schema(array $args)
{
return <<<EOT
SELECT * FROM `destructr_schema`
WHERE `schema_table` = :table
ORDER BY `schema_time` desc
LIMIT 1
EOT;
}
public function update(string $table, DSOInterface $dso): bool
{
if (!$dso->changes() && !$dso->removals()) {
return true;
}
$s = $this->getStatement(
'set_json',
['table' => $table]
);
return $s->execute([
':dso_id' => $dso['dso.id'],
':data' => json_encode($dso->get()),
]);
}
public function delete(string $table, DSOInterface $dso): bool
{
$s = $this->getStatement(
'delete',
['table' => $table]
);
return $s->execute([
':dso_id' => $dso['dso.id'],
]);
}
public function count(string $table, Search $search, array $params): int
{
$s = $this->getStatement(
'count',
['table' => $table, 'search' => $search]
);
if (!$s->execute($params)) {
return null;
}
return intval($s->fetchAll(\PDO::FETCH_COLUMN)[0]);
}
public function select(string $table, Search $search, array $params)
{
$s = $this->getStatement(
'select',
['table' => $table, 'search' => $search]
);
if (!$s->execute($params)) {
return [];
}
return @$s->fetchAll(\PDO::FETCH_ASSOC);
}
public function insert(string $table, DSOInterface $dso): bool
{
return $this->getStatement(
'insert',
['table' => $table]
)->execute(
[':data' => json_encode($dso->get())]
);
}
protected function getStatement(string $type, $args = array()): \PDOStatement
{
$fn = 'sql_' . $type;
if (!method_exists($this, $fn)) {
throw new \Exception("Error getting SQL statement, driver doesn't have a method named $fn");
}
$sql = $this->$fn($args);
$stmt = $this->pdo->prepare($sql);
if (!$stmt) {
$this->lastPreparationErrorOn = $sql;
throw new \Exception("Error preparing statement: " . implode(': ', $this->pdo->errorInfo()), 1);
}
return $stmt;
}
/**
* Within the search we expand strings like ${dso.id} into JSON queries.
* Note that the Search will have already had these strings expanded into
* column names if there are virtual columns configured for them. That
* happens in the Factory before it gets here.
*/
protected function sql_select(array $args): string
{
//extract query parts from Search and expand paths
$where = $this->expandPaths($args['search']->where());
$order = $this->expandPaths($args['search']->order());
$limit = $args['search']->limit();
$offset = $args['search']->offset();
//select from
$out = ["SELECT * FROM `{$args['table']}`"];
//where statement
if ($where !== null) {
$out[] = "WHERE " . $where;
}
//order statement
if ($order !== null) {
$out[] = "ORDER BY " . $order;
}
//limit
if ($limit !== null) {
$out[] = "LIMIT " . $limit;
}
//offset
if ($offset !== null) {
$out[] = "OFFSET " . $offset;
}
//return
return implode(PHP_EOL, $out) . ';';
}
protected function sql_count(array $args): string
{
//extract query parts from Search and expand paths
$where = $this->expandPaths($args['search']->where());
//select from
$out = ["SELECT count(dso_id) FROM `{$args['table']}`"];
//where statement
if ($where !== null) {
$out[] = "WHERE " . $where;
}
//return
return implode(PHP_EOL, $out) . ';';
}
protected function sql_delete(array $args): string
{
return 'DELETE FROM `' . $args['table'] . '` WHERE `dso_id` = :dso_id;';
}
}

View file

@ -1,65 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr\Drivers;
/**
* What this driver supports: MariaDB >= 10.2.7
*/
class MariaDBDriver extends MySQLDriver
{
protected function sql_ddl(array $args = []): string
{
$out = [];
$out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
$lines = [];
$lines[] = "`json_data` JSON DEFAULT NULL";
foreach ($args['schema'] as $path => $col) {
$line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ") VIRTUAL";
$lines[] = $line;
}
$out[] = implode(',' . PHP_EOL, $lines);
$out[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8;";
$out = implode(PHP_EOL, $out);
return $out;
}
protected function buildIndexes(string $table, array $schema): bool
{
foreach ($schema as $path => $col) {
try {
if (@$col['primary']) {
$this->pdo->exec(
"CREATE UNIQUE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING BTREE"
);
} elseif (@$col['unique'] && $as = @$col['index']) {
$this->pdo->exec(
"CREATE UNIQUE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING $as"
);
} elseif ($as = @$col['index']) {
$this->pdo->exec(
"CREATE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING $as"
);
}
} catch (\Throwable $th) {
}
}
return true;
}
protected function addColumns($table, $schema): bool
{
$out = true;
foreach ($schema as $path => $col) {
$line = "ALTER TABLE `{$table}` ADD COLUMN `${col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ")";
if (@$col['primary']) {
$line .= ' PERSISTENT;';
} else {
$line .= ' VIRTUAL;';
}
$out = $out &&
$this->pdo->exec($line) !== false;
}
return $out;
}
}

View file

@ -1,119 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr\Drivers;
/**
* What this driver supports: MySQL >= 5.7.8
*/
class MySQLDriver extends AbstractSQLDriver
{
protected function sql_ddl(array $args = []): string
{
$out = [];
$out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
$lines = [];
$lines[] = "`json_data` JSON DEFAULT NULL";
foreach ($args['schema'] as $path => $col) {
$line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ")";
if (@$col['primary']) {
$line .= ' STORED';
} else {
$line .= ' VIRTUAL';
}
$lines[] = $line;
}
$out[] = implode(',' . PHP_EOL, $lines);
$out[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8;";
$out = implode(PHP_EOL, $out);
return $out;
}
protected function buildIndexes(string $table, array $schema): bool
{
foreach ($schema as $path => $col) {
try {
if (@$col['primary']) {
$this->pdo->exec(
"CREATE UNIQUE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING BTREE"
);
} elseif (@$col['unique'] && $as = @$col['index']) {
$this->pdo->exec(
"CREATE UNIQUE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING $as"
);
} elseif ($as = @$col['index']) {
$this->pdo->exec(
"CREATE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING $as"
);
}
} catch (\Throwable $th) {
}
}
return true;
}
protected function expandPath(string $path): string
{
return "JSON_UNQUOTE(JSON_EXTRACT(`json_data`,'$.{$path}'))";
}
protected function sql_set_json(array $args): string
{
return 'UPDATE `' . $args['table'] . '` SET `json_data` = :data WHERE `dso_id` = :dso_id;';
}
protected function sql_insert(array $args): string
{
return "INSERT INTO `{$args['table']}` (`json_data`) VALUES (:data);";
}
protected function addColumns($table, $schema): bool
{
$out = true;
foreach ($schema as $path => $col) {
$line = "ALTER TABLE `{$table}` ADD COLUMN `${col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ")";
if (@$col['primary']) {
$line .= ' STORED;';
} else {
$line .= ' VIRTUAL;';
}
$out = $out &&
$this->pdo->exec($line) !== false;
}
return $out;
}
protected function removeColumns($table, $schema): bool
{
$out = true;
foreach ($schema as $path => $col) {
$out = $out &&
$this->pdo->exec("ALTER TABLE `{$table}` DROP COLUMN `${col['name']}`;") !== false;
}
return $out;
}
protected function rebuildSchema($table, $schema): bool
{
//this does nothing in databases that can generate columns themselves
return true;
}
protected function sql_create_schema_table(): string
{
return <<<EOT
CREATE TABLE `destructr_schema` (
`schema_time` bigint NOT NULL,
`schema_table` varchar(100) NOT NULL,
`schema_schema` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`schema_schema`)),
PRIMARY KEY (`schema_time`,`schema_table`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
EOT;
}
protected function sql_table_exists(string $table): string
{
$table = preg_replace('/[^a-zA-Z0-9\-_]/', '', $table);
return 'SELECT 1 FROM ' . $table . ' LIMIT 1';
}
}

View file

@ -1,272 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr\Drivers;
use Destructr\DSOInterface;
/**
* What this driver supports: Any version of SQLite3 in PHP environments that allow
* pdo::sqliteCreateFunction
*
* Note that unlike databases with native JSON functions, this driver's generated
* columns are NOT generated in the database. They are updated by this class whenever
* the data they reference changes. This doesn't matter much if you're doing all
* your updating through Destructr, but is something to be cognizant of if your
* data is being updated outside Destructr.
*/
class SQLiteDriver extends AbstractSQLDriver
{
public function update(string $table, DSOInterface $dso): bool
{
if (!$dso->changes() && !$dso->removals()) {
return true;
}
$columns = $this->dso_columns($dso);
$s = $this->getStatement(
'set_json',
[
'table' => $table,
'columns' => $columns,
]
);
return $s->execute($columns);
}
public function insert(string $table, DSOInterface $dso): bool
{
$columns = $this->dso_columns($dso);
$s = $this->getStatement(
'insert',
[
'table' => $table,
'columns' => $columns,
]
);
return $s->execute($columns);
}
protected function updateTable($table, $schema): bool
{
$current = $this->getSchema($table);
if (!$current || $schema == $current) {
return true;
}
//create new table
$table_tmp = "{$table}_tmp_" . md5(rand());
$sql = $this->sql_ddl([
'table' => $table_tmp,
'schema' => $schema,
]);
if ($this->pdo->exec($sql) === false) {
return false;
}
//copy data into it
$sql = ["INSERT INTO $table_tmp"];
$cols = ["json_data"];
$srcs = ["json_data"];
foreach ($schema as $path => $col) {
$cols[] = $col['name'];
$srcs[] = $this->expandPath($path);
}
$sql[] = '(' . implode(',', $cols) . ')';
$sql[] = 'SELECT';
$sql[] = implode(',', $srcs);
$sql[] = "FROM $table";
$sql = implode(PHP_EOL, $sql);
if ($this->pdo->exec($sql) === false) {
return false;
}
//remove old table, rename new table to old table
if ($this->pdo->exec("DROP TABLE $table") === false) {
return false;
}
if ($this->pdo->exec("ALTER TABLE $table_tmp RENAME TO $table") === false) {
return false;
}
//set up indexes
if (!$this->buildIndexes($table, $schema)) {
return false;
}
//save schema
$this->saveSchema($table, $schema);
//return result
return true;
}
protected function addColumns($table, $schema): bool
{
//does nothing
return true;
}
protected function removeColumns($table, $schema): bool
{
//does nothing
return true;
}
protected function rebuildSchema($table, $schema): bool
{
//does nothing
return true;
}
protected function sql_insert(array $args): string
{
$out = [];
$names = array_map(
function ($e) {
return preg_replace('/^:/', '', $e);
},
array_keys($args['columns'])
);
$out[] = 'INSERT INTO `' . $args['table'] . '`';
$out[] = '(`' . implode('`,`', $names) . '`)';
$out[] = 'VALUES (:' . implode(',:', $names) . ')';
$out = implode(PHP_EOL, $out) . ';';
return $out;
}
protected function sql_set_json(array $args): string
{
$names = array_map(
function ($e) {
return '`' . preg_replace('/^:/', '', $e) . '` = ' . $e;
},
array_keys($args['columns'])
);
$out = [];
$out[] = 'UPDATE `' . $args['table'] . '`';
$out[] = 'SET';
$out[] = implode(',' . PHP_EOL, $names);
$out[] = 'WHERE `dso_id` = :dso_id';
$out = implode(PHP_EOL, $out) . ';';
return $out;
}
public function delete(string $table, DSOInterface $dso): bool
{
$s = $this->getStatement(
'delete',
['table' => $table]
);
return $s->execute([
':dso_id' => $dso['dso.id'],
]);
}
/**
* Used to extract a list of column/parameter names for a given DSO, based
* on the current values.
*
* @param DSOInterface $dso
* @return void
*/
protected function dso_columns(DSOInterface $dso)
{
$columns = [':json_data' => json_encode($dso->get())];
foreach ($this->getSchema($dso->factory()->table()) ?? [] as $vk => $vv) {
$columns[':' . $vv['name']] = $dso->get($vk);
}
return $columns;
}
/**
* Intercept calls to set PDO, and add a custom function to SQLite so that it
* can extract JSON values. It's not actually terribly slow, and allows us to
* use JSON seamlessly, almost as if it were native.
*
* @param \PDO $pdo
* @return \PDO|null
*/
public function pdo(\PDO $pdo = null): ?\PDO
{
if ($pdo) {
$this->pdo = $pdo;
$this->pdo->sqliteCreateFunction(
'DESTRUCTR_JSON_EXTRACT',
'\\Destructr\\Drivers\\SQLiteDriver::JSON_EXTRACT',
2
);
}
return $this->pdo;
}
public static function JSON_EXTRACT($json, $path)
{
$path = substr($path, 2);
$path = explode('.', $path);
$arr = json_decode($json, true);
$out = &$arr;
while ($key = array_shift($path)) {
if (isset($out[$key])) {
$out = &$out[$key];
} else {
return null;
}
}
return @"$out";
}
protected function buildIndexes(string $table, array $schema): bool
{
$result = true;
foreach ($schema as $key => $vcol) {
try {
if (@$vcol['primary']) {
//sqlite automatically creates this index
} elseif (@$vcol['unique']) {
$result = $result &&
$this->pdo->exec('CREATE UNIQUE INDEX ' . $table . '_' . $vcol['name'] . '_idx on `' . $table . '`(`' . $vcol['name'] . '`)') !== false;
} elseif (@$vcol['index']) {
$idxResult = $result &&
$this->pdo->exec('CREATE INDEX ' . $table . '_' . $vcol['name'] . '_idx on `' . $table . '`(`' . $vcol['name'] . '`)') !== false;
}
} catch (\Throwable $th) {
}
}
return $result;
}
protected function sql_ddl(array $args = []): string
{
$out = [];
$out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
$lines = [];
$lines[] = "`json_data` TEXT DEFAULT NULL";
foreach ($args['schema'] as $path => $col) {
$line = "`{$col['name']}` {$col['type']}";
if (@$col['primary']) {
$line .= ' PRIMARY KEY';
}
$lines[] = $line;
}
$out[] = implode(',' . PHP_EOL, $lines);
$out[] = ");";
$out = implode(PHP_EOL, $out);
return $out;
}
protected function expandPath(string $path): string
{
return "DESTRUCTR_JSON_EXTRACT(`json_data`,'$.{$path}')";
}
protected function sql_create_schema_table(): string
{
return <<<EOT
CREATE TABLE IF NOT EXISTS `destructr_schema`(
schema_time BIGINT NOT NULL,
schema_table VARCHAR(100) NOT NULL,
schema_schema TEXT NOT NULL
);
EOT;
}
protected function sql_table_exists(string $table): string
{
$table = preg_replace('/[^a-zA-Z0-9\-_]/', '', $table);
return 'SELECT 1 FROM ' . $table . ' LIMIT 1';
}
}

View file

@ -0,0 +1,24 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\Exceptions;
class Exception extends \Exception
{
}

View file

@ -0,0 +1,26 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\Exceptions;
class IDExistsException extends Exception
{
public function __construct($id)
{
parent::__construct("DataObject ID $id already exists -- create() may have been called twice", 1);
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\Exceptions;
class IDSpaceException extends Exception
{
public function __construct()
{
parent::__construct("Maximum ID generation attempts exceeded -- ID space may be exhausted", 1);
}
}

View file

@ -0,0 +1,26 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\Exceptions;
class InvalidEnumException extends Exception
{
public function __construct($value)
{
parent::__construct("Value \"$value\" is not a valid enum value for this field", 1);
}
}

View file

@ -1,324 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr;
use Destructr\Drivers\AbstractDriver;
/**
* The Factory is responsible for keeping track of which columns may or may not
* be configured as virtual columns (although in the future for NoSQL databases
* this may not be relevant).
*
* The overall responsibilities of the Factory are:
* * Tracking which table is to be used
* * Holding the driver to be used
* * Creating DSOs and passing itself to them
* * Calling its own and the DSO's hook_create() and hook_update() methods
* * Passing DSOs that need CRUD-ing to the appropriate Driver methods
* * Creating Search objects
* * Executing Searches (which largely consists of passing them to the Driver)
* * Inspecting unstructured data straight from the database and figuring out what class to make it (defaults to just DSO)
*/
class Factory
{
const ID_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789';
const ID_LENGTH = 8;
/**
* @var Drivers\AbstractDriver
*/
protected $driver;
/**
* @var string
*/
protected $table;
/**
* Virtual columns that should be created for sorting/indexing in the SQL server
*/
protected $schema = [
'dso.id' => [
'name' => 'dso_id', //column name to be used
'type' => 'VARCHAR(16)', //column type
'index' => 'BTREE', //whether/how to index
'unique' => true, //whether column should be unique
'primary' => true, //whether column should be the primary key
],
'dso.type' => [
'name' => 'dso_type',
'type' => 'VARCHAR(30)',
'index' => 'BTREE',
],
'dso.deleted' => [
'name' => 'dso_deleted',
'type' => 'BIGINT',
'index' => 'BTREE',
],
];
public function __construct(Drivers\AbstractDriver $driver, string $table)
{
$this->driver = $driver;
$this->table = $table;
}
public function checkEnvironment(): bool
{
return $this->driver->checkEnvironment(
$this->table,
$this->schema
);
}
public function prepareEnvironment(): bool
{
return $this->driver->prepareEnvironment(
$this->table,
$this->schema
);
}
public function updateEnvironment(): bool
{
return $this->driver->updateEnvironment(
$this->table,
$this->schema
);
}
public function table(): string
{
return $this->table;
}
public function driver(): AbstractDriver
{
return $this->driver;
}
public function tableExists(): bool
{
return $this->driver->tableExists($this->table);
}
public function createSchemaTable(): bool
{
$this->driver->createSchemaTable(AbstractDriver::SCHEMA_TABLE);
return $this->driver->tableExists(AbstractDriver::SCHEMA_TABLE);
}
public function quote(string $str): string
{
return $this->driver->pdo()->quote($str);
}
protected function hook_create(DSOInterface $dso)
{
if (!$dso->get('dso.id')) {
$dso->set('dso.id', static::generate_id(static::ID_CHARS, static::ID_LENGTH), true);
}
if (!$dso->get('dso.created.date')) {
$dso->set('dso.created.date', time());
}
if (!$dso->get('dso.created.user')) {
$dso->set('dso.created.user', ['ip' => @$_SERVER['REMOTE_ADDR']]);
}
}
protected function hook_update(DSOInterface $dso)
{
$dso->set('dso.modified.date', time());
$dso->set('dso.modified.user', ['ip' => @$_SERVER['REMOTE_ADDR']]);
}
/**
* Override this function to allow a factory to create different
* sub-classes of DSO based on attributes of the given object's
* data. For example, you could use a property like dso.class to
* select a class from an associative array.
*
* @param array $data
* @return string|null
*/
function class(?array $data): ?string
{
return null;
}
public function delete(DSOInterface $dso, bool $permanent = false): bool
{
if ($permanent) {
return $this->driver->delete($this->table, $dso);
}
$dso['dso.deleted'] = time();
return $this->update($dso, true);
}
public function undelete(DSOInterface $dso): bool
{
unset($dso['dso.deleted']);
return $this->update($dso, true);
}
public function create(?array $data = array()): DSOInterface
{
if (!($class = $this->class($data))) {
$class = DSO::class;
}
$dso = new $class($data, $this);
$this->hook_create($dso);
$dso->hook_create();
$dso->resetChanges();
return $dso;
}
public function schema(): array
{
return $this->driver->getSchema($this->table) ?? $this->schema;
}
protected function virtualColumnName($path): ?string
{
return @$this->schema()[$path]['name'];
}
public function update(DSOInterface $dso, bool $sneaky = false): bool
{
if (!$dso->changes() && !$dso->removals()) {
return true;
}
if (!$sneaky) {
$this->hook_update($dso);
$dso->hook_update();
}
$out = $this->driver->update($this->table, $dso);
$dso->resetChanges();
return $out;
}
public function search(): Search
{
return new Search($this);
}
protected function makeObjectFromRow($arr)
{
$data = json_decode($arr['json_data'], true);
return $this->create($data);
}
protected function makeObjectsFromRows($arr)
{
foreach ($arr as $key => $value) {
$arr[$key] = $this->makeObjectFromRow($value);
}
return $arr;
}
public function executeCount(Search $search, array $params = array(), $deleted = false): ?int
{
//add deletion clause and expand column names
$search = $this->preprocessSearch($search, $deleted);
//run select
return $this->driver->count(
$this->table,
$search,
$params
);
}
public function executeSearch(Search $search, array $params = array(), $deleted = false): array
{
//add deletion clause and expand column names
$search = $this->preprocessSearch($search, $deleted);
//run select
$r = $this->driver->select(
$this->table,
$search,
$params
);
return $this->makeObjectsFromRows($r);
}
public function read(string $value, string $field = 'dso.id', $deleted = false): ?DSOInterface
{
$search = $this->search();
$search->where('${' . $field . '} = :value');
if ($results = $search->execute([':value' => $value], $deleted)) {
return array_shift($results);
}
return null;
}
public function insert(DSOInterface $dso): bool
{
$this->hook_update($dso);
$dso->hook_update();
$dso->resetChanges();
return $this->driver->insert($this->table, $dso);
}
protected function preprocessSearch($input, $deleted)
{
//clone search so we're not accidentally messing with a reference
$search = new Search($this);
$search->where($input->where());
$search->order($input->order());
$search->limit($input->limit());
$search->offset($input->offset());
/* add deletion awareness to where clause */
if ($deleted !== null) {
$where = $search->where();
if ($deleted === true) {
$added = '${dso.deleted} is not null';
} else {
$added = '${dso.deleted} is null';
}
if ($where) {
$where = '(' . $where . ') AND ' . $added;
} else {
$where = $added;
}
$search->where($where);
}
/* expand virtual column names */
foreach (['where', 'order'] as $clause) {
if ($value = $search->$clause()) {
$value = preg_replace_callback(
'/\$\{([^\}\\\]+)\}/',
function ($matches) {
/* depends on whether a virtual column is expected for this value */
if ($vcol = $this->virtualColumnName($matches[1])) {
return "`$vcol`";
}
return $matches[0];
},
$value
);
$search->$clause($value);
}
}
/* return search */
return $search;
}
protected static function generate_id($chars, $length, string $prefix = null): string
{
$id = '';
while (strlen($id) < $length) {
$id .= substr(
$chars,
rand(0, strlen($chars) - 1),
1
);
}
if ($prefix) $id = $prefix . '_' . $id;
return $id;
}
public function errorInfo()
{
return $this->driver->errorInfo();
}
}

View file

@ -0,0 +1,156 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\Files;
use \Digraph\DataObject\Files\SingleFile;
class FilesContainer implements FilesContainerInterface, \ArrayAccess, \Iterator
{
protected $files = array();
protected $iterMap = array();
protected $iterPos = 0;
protected $_obj = null;
public function __construct($obj, $storageString)
{
$this->setParent($obj);
$files = @json_decode($storageString, true);
if (!is_array($files)) {
$files = array();
}
foreach ($files as $key => $value) {
$this->addFile($key, $value);
}
}
public function setParent(&$obj)
{
$this->_obj = $obj;
}
public function getStashFolder()
{
return $this->_obj->getStashFolder();
}
public function getStorageFolder()
{
return $this->_obj->getStorageFolder();
}
public function addFile($id, $info)
{
$this[$id] = new SingleFile($this, $info);
}
public function deleteFile($id)
{
unlink($this->files[$id]->fullPath());
unset($this->files[$id]);
$this->buildIterMap();
}
public function getStorageString()
{
$out = array();
foreach ($this as $name => $file) {
$out[$name] = $file->getProperties();
}
return json_encode($out);
}
public function generateTimestampNow()
{
return $this->_obj->generateTimestampNow('FilesContainer', array());
}
public function generateCurrentUser()
{
return $this->_obj->generateCurrentUser('FilesContainer', array());
}
public function fieldCount()
{
return count($this->files);
}
protected function buildIterMap()
{
$this->iterMap = array();
foreach ($this->files as $key => $value) {
$this->iterMap[] = $key;
}
}
public function offsetSet($offset, $value)
{
if (is_null($offset)) {
$this->files[] = $value;
$offset = count($this->files);
} else {
$this->files[$offset] = $value;
}
$value->setParent($this);
$this->buildIterMap();
}
public function offsetExists($offset)
{
return isset($this->files[$offset]);
}
public function offsetUnset($offset)
{
$this->deleteFile($offset);
}
protected function &getRef($offset)
{
return $this->files[$offset];
}
public function offsetGet($offset)
{
if (isset($this->files[$offset])) {
return $this->getRef($offset);
}
return null;
}
public function rewind()
{
$this->iterPos = 0;
}
public function &current()
{
if (isset($this->files[$this->key()])) {
return $this->files[$this->key()];
}
$return = null;
return $return;
}
public function key()
{
return isset($this->iterMap[$this->iterPos]) ? $this->iterMap[$this->iterPos] : null;
}
public function next()
{
$this->iterPos++;
}
public function valid()
{
return isset($this->files[$this->key()]);
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\Files;
use \Digraph\DataObject\DataByReferenceInterface;
interface FilesContainerInterface extends DataByReferenceInterface
{
public function getStashFolder();
public function getStorageFolder();
public function addFile($id, $infoArray);
public function deleteFile($id);
}

214
src/Files/SingleFile.php Normal file
View file

@ -0,0 +1,214 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\Files;
class SingleFile implements SingleFileInterface, \ArrayAccess, \Iterator
{
protected $properties = array(
'name' => 'empty',
'ext' => 'txt',
'type' => 'text/plain',
'size' => 0,
'ctime' => null,
'cuser' => null,
'mtime' => null,
'muser' => null,
'store_name' => null
);
protected $iterMap = array();
protected $iterPos = 0;
protected $writeProtect = array(
'ctime','cuser','mtime','muser','size','type','ext'
);
protected $_container = false;
public function __construct($container, $info)
{
$this->setParent($container);
if (!isset($info['ctime'])) {
$info['ctime'] = $this->generateTimestampNow();
$info['cuser'] = $this->generateCurrentUser();
}
if (!isset($info['mtime'])) {
$info['mtime'] = $this->generateTimestampNow();
$info['muser'] = $this->generateCurrentUser();
}
foreach ($this->properties as $key => $value) {
$this->properties[$key] = isset($info[$key])?$info[$key]:null;
}
if (isset($info['tmp_name'])) {
$this->properties['tmp_name'] = $info['tmp_name'];
}
if (isset($info['stash_name'])) {
$this->properties['stash_name'] = $info['stash_name'];
}
}
public function getProperties()
{
return $this->properties;
}
public function fullPath()
{
if (isset($this['store_name'])) {
return $this->_container->getStorageFolder() . '/' . $this['store_name'];
}
if (isset($this['stash_name'])) {
return $this['stash_name'];
}
if (isset($this['tmp_name'])) {
return $this['tmp_name'];
}
return false;
}
public function store($skipCheck = false)
{
$storeFolder = $this->_container->getStorageFolder();
if (isset($this['tmp_name'])) {
$result = $this->stash($skipCheck);
if ($result == false) {
return false;
}
}
if (isset($this['stash_name'])) {
$storageName = strtolower(preg_replace('/[^a-z0-9]/i', '', $this['name'])).'_'.md5(file_get_contents($this['stash_name'])).'.'.$this['ext'];
$moved = rename($this['stash_name'], $storeFolder . '/' . $storageName);
if ($moved) {
unset($this['stash_name']);
$this['store_name'] = $storageName;
}
return $moved;
}
//return true because nothing needed doing
return true;
}
public function stash($skipCheck = false)
{
$stashFolder = $this->_container->getStashFolder();
if (isset($this['tmp_name'])) {
$stashFile = $stashFolder . '/' . md5(rand());
if ($skipCheck) {
$moved = rename($this['tmp_name'], $stashFile);
} else {
$moved = move_uploaded_file($this['tmp_name'], $stashFile);
}
if ($moved) {
unset($this['tmp_name']);
$this['stash_name'] = $stashFile;
}
return $moved;
}
//return true becuase nothing needed doing
return true;
}
public function setParent($container)
{
$this->_container = $container;
}
public function updateModified()
{
$this->properties['mtime'] = $this->generateTimestampNow();
$this->properties['muser'] = $this->generateCurrentUser();
}
public function generateTimestampNow()
{
return $this->_container->generateTimestampNow();
}
public function generateCurrentUser()
{
return $this->_container->generateCurrentUser();
}
public function fieldCount()
{
return count($this->properties);
}
protected function buildIterMap()
{
$this->iterMap = array();
foreach ($this->properties as $key => $value) {
$this->iterMap[] = $key;
}
}
public function offsetSet($offset, $value)
{
if (in_array($offset, $this->writeProtect)) {
trigger_error("Property \"$offset\" is write-protected", E_USER_WARNING);
return false;
}
if (is_null($offset)) {
$this->properties[] = $value;
$offset = count($this->properties);
} else {
$this->properties[$offset] = $value;
}
$this->updateModified();
$this->buildIterMap();
}
public function offsetExists($offset)
{
return isset($this->properties[$offset]);
}
public function offsetUnset($offset)
{
unset($this->properties[$offset]);
$this->updateModified();
$this->buildIterMap();
}
public function &offsetGet($offset)
{
if (isset($this->properties[$offset])) {
return $this->properties[$offset];
}
$ref = null;
return $ref;
}
public function rewind()
{
$this->iterPos = 0;
}
public function &current()
{
if (isset($this->properties[$this->key()])) {
return $this->properties[$this->key()];
}
return null;
}
public function key()
{
return isset($this->iterMap[$this->iterPos]) ? $this->iterMap[$this->iterPos] : null;
}
public function next()
{
$this->iterPos++;
}
public function valid()
{
return isset($this->properties[$this->key()]);
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\Files;
interface SingleFileInterface
{
public function __construct($container, $properties);
public function setParent($container);
public function generateTimestampNow();
public function generateCurrentUser();
public function store($skipCheck = false);
public function stash($skipCheck = false);
public function getProperties();
}

View file

@ -0,0 +1,232 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\SQL;
use \Digraph\DataObject\Exceptions\IDExistsException;
use \Digraph\DataObject\Exceptions\UnmappedFieldException;
use \Digraph\DataObject\SQL\Exceptions\QueryException;
abstract class AbstractSQLDataObject extends \Digraph\DataObject\AbstractArrayDataObject implements SQLDataObjectInterface
{
protected static $_table;
protected static $_conn = null;
protected static $_fpdo = null;
protected static function getConn()
{
if (static::$_conn === null) {
static::$_conn = static::buildConn();
}
return static::$_conn;
}
protected static function getFPDO()
{
if (static::$_fpdo === null) {
static::$_fpdo = new \FluentPDO(static::getConn());
}
return static::$_fpdo;
}
protected static function getInsert()
{
return static::getFPDO()->insertInto(static::$_table);
}
public static function getSelect($deleted = false)
{
$query = static::getFPDO()->from(static::$_table);
if (!$deleted) {
$deletedCol = static::storageName('do_deleted');
$query->where("$deletedCol is null");
}
return $query;
}
protected function getUpdate()
{
return static::getFPDO()->update(static::$_table)->where(
static::storageName('do_id'),
$this->do_id
);
}
public static function runSelect($query)
{
$class = get_called_class();
$results = array();
foreach ($query as $row) {
$data = array();
foreach (static::map() as $key => $mapEntry) {
$data[$key] = $row[$mapEntry['name']];
}
$results[] = new $class($data, true);
}
return $results;
}
public function create($dump = false)
{
if (static::idExists($this->do_id)) {
throw new IDExistsException($this->do_id);
}
$values = array();
foreach ($this->dataRaw as $name => $value) {
$map = $this->mapEntry($name);
$values[$map['name']] = $value;
}
$query = $this->getInsert();
$query->values($values);
if ($dump) {
var_dump($query->getQuery());
var_dump($values);
}
$result = $query->execute();
if ($result === false) {
throw new QueryException();
}
return $result;
}
public static function read($id, $deleted = false)
{
$query = static::getSelect($deleted);
$query->where(static::storageName('do_id'), $id);
$results = static::runSelect($query);
return array_pop($results);
}
protected static function isValidOrder($order)
{
switch (strtolower($order)) {
case 'asc':
return true;
case 'desc':
return true;
default:
return false;
}
}
protected static function colParameterSearch($str)
{
$class = get_called_class();
$str = preg_replace_callback('/(:([a-zA-Z0-9_]+))/', function ($matches) use ($class) {
$col = $class::storageName($matches[2]);
if (!$col) {
throw new UnmappedFieldException($matches[2]);
}
return $col;
}, $str);
return $str;
}
public static function count($parameters = array())
{
$query = static::getSelect();
//parse $parameters
foreach ($parameters as $search => $value) {
$search = static::colParameterSearch($search);
if ($value === null) {
$query->where($search);
} else {
$query->where($search, $value);
}
}
return count($query);
}
public static function search($parameters = array(), $sort = array(), $options = array())
{
$deleted = (isset($options['deleted']) && $options['deleted']);
$query = static::getSelect($deleted);
//parse $parameters
foreach ($parameters as $search => $value) {
$search = static::colParameterSearch($search);
if ($value === null) {
$query->where($search);
} else {
$query->where($search, $value);
}
}
//parse $sort
foreach ($sort as $name => $order) {
if (($col = static::colParameterSearch($name)) && static::isValidOrder($order)) {
$query->orderBy("$col $order");
} else {
throw new UnmappedFieldException($name);
}
}
//limit
if (isset($options['limit'])) {
$query->limit($options['limit']);
}
//offset
if (isset($options['offset'])) {
$query->offset($options['offset']);
}
return static::runSelect($query);
}
public function update($action = null, $dump = false)
{
// var_dump($this->dataChanged);
// exit();
if ($action !== null) {
$action = array('action'=>$action);
} else {
$action = array();
}
if (!$this->dataChanged) {
return null;
}
$this->set('do_mdate', strval(time()));
$this->set('do_muser', $this->generateUser('do_muser'));
$query = static::getUpdate();
$values = array();
foreach ($this->dataChanged as $key) {
$values[static::storageName($key)] = $this->getRaw($key);
}
$query->set($values);
if ($dump) {
var_dump($query->getQuery());
var_dump($values);
}
return $query->execute();
}
public function delete($permanent = false, $dump = false)
{
if ($permanent) {
$query = $this->getFPDO()->deleteFrom(static::$_table);
$query->where(static::storageName('do_id'), $this->do_id);
return $query->execute();
}
$this->set('do_deleted', time());
return $this->update(null, $dump);
}
public static function idExists($id)
{
if (static::read($id)) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,24 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\SQL\Exceptions;
class Exception extends \Digraph\DataObject\Exceptions\Exception
{
}

View file

@ -0,0 +1,26 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\SQL\Exceptions;
class QueryException extends Exception
{
public function __construct()
{
parent::__construct("A query threw an exception. A good place to look for problems is the column names in map()", 1);
}
}

View file

@ -0,0 +1,24 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\SQL;
interface SQLDataObjectInterface extends \Digraph\DataObject\DataObjectInterface
{
public static function runSelect($query);
public static function getSelect($deleted = false);
}

View file

@ -1,91 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr;
class Search implements \Serializable
{
protected $factory;
protected $where;
protected $order;
protected $limit;
protected $offset;
public function __construct(Factory $factory = null)
{
$this->factory = $factory;
}
public function quote(string $str): string
{
return $this->factory->quote($str);
}
public function count(array $params = array(), $deleted = false)
{
return $this->factory->executeCount($this, $params, $deleted);
}
public function execute(array $params = array(), $deleted = false)
{
return $this->factory->executeSearch($this, $params, $deleted);
}
public function where(string $set = null): ?string
{
if ($set !== null) {
$this->where = $set;
}
return $this->where;
}
public function paginate(int $perPage, int $page = 1)
{
$this->limit($perPage);
$this->offset(($page - 1) * $perPage);
}
public function pageCount(int $perPage)
{
return ceil($this->count() / $perPage);
}
public function order(string $set = null): ?string
{
if ($set !== null) {
$this->order = $set;
}
return $this->order;
}
public function limit(int $set = null): ?int
{
if ($set !== null) {
$this->limit = $set;
}
return $this->limit;
}
public function offset(int $set = null): ?int
{
if ($set !== null) {
$this->offset = $set;
}
return $this->offset;
}
public function serialize()
{
return json_encode(
[$this->where(), $this->order(), $this->limit(), $this->offset()]
);
}
public function unserialize($string)
{
list($where, $order, $limit, $offset) = json_decode($string, true);
$this->where($where);
$this->order($order);
$this->limit($limit);
$this->offset($offset);
}
}

128
src/Utils/BadWords.php Normal file
View file

@ -0,0 +1,128 @@
<?php
/**
* Digraph CMS: DataObject
* https://github.com/digraphcms/digraph-dataobject
* Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*/
namespace Digraph\DataObject\Utils;
/**
* A very aggressive profanity filter, used in AbstractDataObject to make an
* attempt at not generating IDs with profanity in them.
*/
class BadWords
{
protected static $_setup = false;
protected static $_dict = array();
protected static $_dictSrc = array();
protected static $_leet = array();
protected static $_leetSrc = array();
/**
* Check whether a piece of text contains anything that looks like a bad word
* @param string $text
* @return bool
*/
public static function profane($text)
{
if (!static::$_setup) {
static::init();
}
foreach (static::$_dict as $pattern) {
if (preg_match('/'.$pattern.'/i', $text)) {
return true;
}
}
return false;
}
/**
* Init class with default words and leet replacements
* @return void
*/
protected static function init()
{
//add and prep leet replacements
$leet = explode(PHP_EOL, trim(file_get_contents(__DIR__.'/_resources/leet.txt')));
foreach ($leet as $replacement) {
$replacement = explode(' ', $replacement);
static::addLeet($replacement[0], $replacement[1], true);
}
static::prepLeet();
//add and prep dictionary
foreach (explode(PHP_EOL, trim(file_get_contents(__DIR__.'/_resources/badwords.txt'))) as $word) {
static::addWord($word, true);
}
static::prepDict();
}
/**
* add a Leet replacement
* @param string $a original
* @param string $b replacement
* @param boolean $skipPrep whether to run prepLeet afterwards
*/
public static function addLeet($a, $b, $skipPrep = false)
{
if (!isset(static::$_leet[$a])) {
static::$_leetSrc[$a] = array($a=>$a);
}
static::$_leetSrc[$a][$b] = $b;
if (!$skipPrep) {
static::prepLeet();
}
}
/**
* parse added Leet rules into a set of regex rules
* @return void
*/
public static function prepLeet()
{
static::$_leet = array();
foreach (static::$_leetSrc as $key => $value) {
static::$_leet[$key] = '('.implode('|', $value).')';
}
}
/**
* add a profane word
* @param string $word
* @param boolean $skipPrep whether to run prepDict afterwards
*/
public static function addWord($word, $skipPrep = false)
{
static::$_dictSrc[] = $word;
static::$_dictSrc = array_unique(static::$_dictSrc);
if (!$skipPrep) {
static::prepDict();
}
}
/**
* parse added words and Leet rules into a set of regex rules
* @return void
*/
public static function prepDict()
{
static::$_dict = array();
foreach (static::$_dictSrc as $word) {
foreach (static::$_leet as $a => $b) {
$word = str_replace($a, $b, $word);
}
static::$_dict[] = $word;
}
}
}

View file

@ -0,0 +1,62 @@
an(al|us)
arse
ass
balls
bastard
bia?tch
blood
blowjob
b(o|u)ll?oc?k
boner
boob
butt
chink
clit
cock
coon
crap
cuck
cunt
damn
dick
dildo
dyke
fag
feck
fellat
felch
fuck
fudgepacker
flange
hell
homo
jerk
jizz
knob
kike
kyke
labia
muff
nigg?(er)?
penis
piss
poop
prick
pr0n
pube
pussy
queer
scrotum
sex
shit
slut
smegma
spunk
tit
tosser
turd
twat
vagina
wank
whore
wtf

View file

@ -0,0 +1,15 @@
a 4
b 8
ck xx?
ex ecks
e 3
f ph
g 6
i l
i 1
o 0
qu kw
s 5
s z
t 7
z 2

View file

@ -0,0 +1,64 @@
anal
anus
arse
ass
balls
bastard
bitch
biatch
blood
blowjob
bollo
boner
boob
butt
chink
clit
cock
coon
crap
cuck
cunt
damn
dick
dildo
dyke
fag
feck
fellat
felching
fuck
fudgepacker
flange
hell
homo
jerk
jizz
knob
kike
kyke
labia
muff
nig
penis
piss
poop
prick
pr0n
pube
pussy
queer
scrotum
sex
shit
slut
smegma
spunk
tit
tosser
turd
twat
vagina
wank
whore
wtf

12
src/_resources/leet.txt Normal file
View file

@ -0,0 +1,12 @@
a 4
b 8
e 3
f ph
g 6
i l
i 1
o 0
s 5
t 7
z 2
s z

BIN
test.sqlite Normal file

Binary file not shown.

View file

@ -0,0 +1,125 @@
<?php
namespace Digraph\DataObject\Tests;
use PHPUnit\Framework\TestCase;
use Digraph\DataObject\Tests\AbstractArrayHarnessObject;
class AbstractArrayDataObjectTest extends TestCase
{
public function testFundamentalGetters()
{
$testObj = new AAOT();
$this->assertEquals(
'testPropDefault-gotten',
$testObj["testProp1"],
"Getter set in map isn't altering output"
);
$this->assertEquals(
'testPropDefault',
$testObj["testProp1Raw"],
"Getter set via function name (_get_testProp1Raw) isn't working"
);
$this->assertEquals(
null,
$testObj["doesNotExist"],
"getting must return null for nonexistent properties"
);
$this->assertEquals(
null,
$testObj["testProp4"],
"getting must return null for masked properties"
);
}
public function testFundamentalIssetters()
{
$testObj = new AAOT();
$this->assertFalse(
isset($testObj["doesNotExist"]),
"isset() must return false for items that don't exist"
);
$this->assertTrue(
isset($testObj["testProp1"]),
"isset() must return true for items with custom getters in the map"
);
$this->assertTrue(
isset($testObj["testProp1Raw"]),
"isset() must return true for custom get functions like _get_testProp1Raw()"
);
$this->assertTrue(
isset($testObj["testProp2"]),
"isset() must return true for items with no custom getter"
);
$this->assertFalse(
isset($testObj["testProp4"]),
"isset() must return false for masked properties"
);
}
public function testFundamentalSetters()
{
$testObj = new AAOT();
$testObj["testProp2"] = 'testPropSet';
$this->assertEquals(
'testPropSet',
$testObj["testProp2"],
"Setting an item with no custom setter isn't working"
);
$testData = array(
"test",
"data",
array("nested")
);
$testObj["testProp3"] = $testData;
$this->assertEquals(
$testData[0],
$testObj["testProp3"][0],
"JSON setter assigned in map isn't working"
);
}
}
class AAOT extends AbstractArrayHarnessObject
{
static $TESTPROP1 = array(
'name' => 'testprop1',
'default' => 'testPropDefault',
'transform' => 'testGetter'
);
static $TESTPROP2 = array(
'name' => 'testprop2',
'default' => 'testProp2Default'
);
static $TESTPROP3 = array(
'name' => 'testProp3',
'transform' => 'JSON'
);
static $TESTPROP4 = array(
'name' => 'testProp4',
'masked' => true,
'default' => 'testProp4Default'
);
static function getMap()
{
$map = parent::getMap();
$map['testProp1'] = static::$TESTPROP1;
$map['testProp2'] = static::$TESTPROP2;
$map['testProp3'] = static::$TESTPROP3;
$map['testProp4'] = static::$TESTPROP4;
return $map;
}
public function _getter_test($name)
{
return $this->getRaw($name) . '-gotten';
}
public function _get_testProp1Raw($name)
{
return $this->getRaw('testProp1');
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Digraph\DataObject\Tests;
use PHPUnit\Framework\TestCase;
use Digraph\DataObject\AbstractArrayDataObject;
class AbstractArrayHarnessObject extends AbstractArrayDataObject
{
protected static $_classTransforms = array(
'testGetter' => array(
'get' => 'testGetter'
),
'testSetter' => array(
'set' => 'testSetter'
)
);
public function testGetter($name)
{
return $this->getRaw($name) . '-gotten';
}
function create()
{
}
static function read($id)
{
}
static function search($parameters = array(), $sort = array(), $options = array())
{
}
function update($action = null)
{
}
function delete($permanent = false)
{
}
static function idExists($id)
{
return false;
}
}

View file

@ -0,0 +1,166 @@
<?php
namespace Digraph\DataObject\Tests;
use PHPUnit\Framework\TestCase;
use Digraph\DataObject\Tests\AbstractHarnessObject;
class AbstractDataObjectTest extends TestCase
{
public function testMap()
{
$baseMap = AbstractHarnessObject::map();
$testMap = ADOT::map();
$this->assertEquals(
4,
count($testMap)-count($baseMap),
"The test harness object doesn't have the right number of map items"
);
$this->assertEquals(
ADOT::$TESTPROP1,
ADOT::mapEntry('testProp1')
);
}
/**
* @expectedException Digraph\DataObject\Exceptions\IDSpaceException
*/
function testIDSpaceExhaustion()
{
new ADOT_BrokenIDExists();
}
public function testGeneration()
{
$testObj = new ADOT(array(
'testProp2' => 'manuallySet'
));
//cdate and mdate should be very close to now
$this->assertLessThan(
1,
time()-$testObj->do_cdate,
"Newly created object's do_cdate is more than 1 second from now"
);
$this->assertLessThan(
1,
time()-$testObj->do_mdate,
"Newly created object's do_mdate is more than 1 second from now"
);
$this->assertEquals(
'manuallySet',
$testObj->testProp2,
"Newly created object's property not correctly set from array"
);
}
public function testGetters()
{
$testObj = new ADOT();
$this->assertEquals(
'testPropDefault-gotten',
$testObj->testProp1,
"Getter set in map isn't altering output"
);
$this->assertEquals(
'testPropDefault',
$testObj->testProp1Raw,
"Getter set via function name (_get_testProp1Raw) isn't working"
);
$this->assertEquals(
null,
$testObj->doesNotExist,
"__get() must return null for nonexistent properties"
);
$this->assertEquals(
null,
$testObj->testProp4,
"__get() must return null for masked properties"
);
}
public function testIssetters()
{
$testObj = new ADOT();
$this->assertFalse(
isset($testObj->doesNotExist),
"__isset() must return false for items that don't exist"
);
$this->assertTrue(
isset($testObj->testProp1),
"__isset() must return true for items with custom getters in the map"
);
$this->assertTrue(
isset($testObj->testProp1Raw),
"__isset() must return true for custom get functions like _get_testProp1Raw()"
);
$this->assertTrue(
isset($testObj->testProp2),
"__isset() must return true for items with no custom getter"
);
$this->assertFalse(
isset($testObj->testProp4),
"__isset() must return false for masked properties"
);
}
public function testSetters()
{
$testObj = new ADOT();
$testObj->testProp2 = 'testPropSet';
$this->assertEquals(
'testPropSet',
$testObj->testProp2,
"Setting an item with no custom setter isn't working"
);
}
}
class ADOT extends AbstractHarnessObject
{
static $TESTPROP1 = array(
'name' => 'testprop1',
'default' => 'testPropDefault',
'transform' => 'testGetter'
);
static $TESTPROP2 = array(
'name' => 'testprop2',
'default' => 'testProp2Default'
);
static $TESTPROP3 = array(
'name' => 'testProp3',
'transform' => 'JSON'
);
static $TESTPROP4 = array(
'name' => 'testProp4',
'masked' => true,
'default' => 'testProp4Default'
);
static function getMap()
{
$map = parent::getMap();
$map['testProp1'] = static::$TESTPROP1;
$map['testProp2'] = static::$TESTPROP2;
$map['testProp3'] = static::$TESTPROP3;
$map['testProp4'] = static::$TESTPROP4;
return $map;
}
public function _get_testProp1Raw($name)
{
return $this->getRaw('testProp1');
}
}
class ADOT_BrokenIDExists extends ADOT
{
static function idExists($id)
{
return true;
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Digraph\DataObject\Tests;
use PHPUnit\Framework\TestCase;
use Digraph\DataObject\AbstractArrayDataObject;
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
class AbstractHarnessObject extends AbstractArrayDataObject
{
protected static $_classTransforms = array(
'testGetter' => array(
'get' => 'testGetter'
),
'testSetter' => array(
'set' => 'testSetter'
)
);
public function testGetter($name)
{
return $this->getRaw($name) . '-gotten';
}
public function create()
{
}
public static function read($id)
{
}
public static function search($parameters = array(), $sort = array(), $options = array())
{
}
public function update($action = null)
{
}
public function delete($permanent = false)
{
}
public static function idExists($id)
{
return false;
}
}

View file

@ -1,227 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr;
use PHPUnit\Framework\TestCase;
class DSOTest extends TestCase
{
public function testChangeTracking()
{
$dso = new DSO([
'a' => 'b',
'c' => 'd',
'e' => [1,2,3]
]);
$this->assertEquals([], $dso->changes());
$this->assertEquals([], $dso->removals());
//not actually a change, shouldn't trigger
$dso['a'] = 'b';
$this->assertEquals([], $dso->changes());
$this->assertEquals([], $dso->removals());
//not actually a change, shouldn't trigger
$dso['e'] = [1,2,3];
$this->assertEquals([], $dso->changes());
$this->assertEquals([], $dso->removals());
//changing a should trigger changes but not removals
$dso['a'] = 'B';
$this->assertEquals(['a'=>'B'], $dso->changes());
$this->assertEquals([], $dso->removals());
//removing c should trigger removals but not change changes
unset($dso['c']);
$this->assertEquals(['a'=>'B'], $dso->changes());
$this->assertEquals(['c'=>'d'], $dso->removals());
//setting c back should remove it from removals, but add it to changes
$dso['c'] = 'd';
$this->assertEquals(['a'=>'B','c'=>'d'], $dso->changes());
$this->assertEquals([], $dso->removals());
//unsetting c again should remove it from changes, but add it back to removals
unset($dso['c']);
$this->assertEquals(['a'=>'B'], $dso->changes());
$this->assertEquals(['c'=>'d'], $dso->removals());
//resetting changes
$dso->resetChanges();
$this->assertEquals([], $dso->changes());
$this->assertEquals([], $dso->removals());
}
public function testGetting()
{
$data = [
'a' => 'A',
'b' => ['c'=>'C']
];
$a = new DSO($data);
//first level
$this->assertEquals('A', $a['a']);
$this->assertEquals('A', $a->get('a'));
//nested
$this->assertEquals('C', $a['b.c']);
$this->assertEquals('C', $a->get('b.c'));
//returning array
$this->assertEquals(['c'=>'C'], $a['b']);
$this->assertEquals(['c'=>'C'], $a->get('b'));
//returning entire array by requesting null or empty string
$this->assertEquals($data, $a[null]);
$this->assertEquals($data, $a->get());
$this->assertEquals($data, $a['']);
$this->assertEquals($data, $a->get(''));
//requesting invalid keys should return null
$this->assertNull($a->get('nonexistent'));
$this->assertNull($a->get('b.nonexistent'));
$this->assertNull($a->get('..'));
$this->assertNull($a->get('.'));
//double dots
$this->assertNull($a->get('..a'));
$this->assertNull($a->get('a..'));
$this->assertNull($a->get('..a..'));
$this->assertNull($a->get('..a..'));
$this->assertNull($a->get('b..c'));
$this->assertNull($a->get('b..c..'));
$this->assertNull($a->get('..b..c'));
$this->assertNull($a->get('..b..c..'));
$this->assertNull($a->get('b.c..'));
$this->assertNull($a->get('..b.c'));
$this->assertNull($a->get('..b.c..'));
//single dots
$this->assertNull($a->get('.a'));
$this->assertNull($a->get('a.'));
$this->assertNull($a->get('.a.'));
$this->assertNull($a->get('.a.'));
$this->assertNull($a->get('b.c.'));
$this->assertNull($a->get('.b.c'));
$this->assertNull($a->get('.b.c.'));
$this->assertNull($a->get('b.c.'));
$this->assertNull($a->get('.b.c'));
$this->assertNull($a->get('.b.c.'));
}
public function testSetting()
{
$data = [
'a' => 'A',
'b' => ['c'=>'C']
];
$a = new DSO($data);
//setting on first layer
$a['a'] = 'B';
$this->assertEquals('B', $a['a']);
$a['new'] = 'NEW';
$this->assertEquals('NEW', $a['new']);
//setting nested
$a['b.c'] = 'D';
$this->assertEquals('D', $a['b.c']);
$a['b.new'] = 'NEW';
$this->assertEquals('NEW', $a['b.new']);
//final state
$this->assertEquals(
[
'a' => 'B',
'b' => [
'c' => 'D',
'new' => 'NEW'
],
'new' => 'NEW'
],
$a->get()
);
}
public function testSettingFalseyValues()
{
$a = new DSO(['foo'=>['bar'=>'baz']]);
$a['foo.bar'] = false;
$this->assertFalse($a['foo.bar']);
$a = new DSO(['foo'=>['bar'=>'baz']]);
$a['foo.bar'] = 0;
$this->assertSame(0, $a['foo.bar']);
$a = new DSO(['foo'=>['bar'=>'baz']]);
$a['foo.bar'] = '';
$this->assertSame('', $a['foo.bar']);
$a = new DSO(['foo'=>['bar'=>'baz']]);
$a['foo.bar'] = [];
$this->assertIsArray($a['foo.bar']);
}
public function testMergingFalseyValues()
{
$a = new DSO(['foo'=>['bar'=>'baz']]);
$a->merge(['foo'=>['bar'=>false]], null, true);
$this->assertFalse($a['foo.bar']);
$a = new DSO(['foo'=>['bar'=>'baz']]);
$a->merge(['foo'=>['bar'=>0]], null, true);
$this->assertSame(0, $a['foo.bar']);
$a = new DSO(['foo'=>['bar'=>'baz']]);
$a->merge(['foo'=>['bar'=>'']], null, true);
$this->assertSame('', $a['foo.bar']);
$a = new DSO(['foo'=>['bar'=>'baz']]);
$a->merge(['foo'=>['bar'=>[]]], null, true);
$this->assertIsArray($a['foo.bar']);
}
public function testMerge()
{
$data = [
'a' => 'b',
'c' => [
'd' => 'e'
]
];
//overwrite false, original values should be preserved
$c = new DSO($data);
$c->merge([
'a' => 'B',
'c' => [
'd' => 'E',
'f' => 'g'
],
'h' => 'i'
]);
$this->assertEquals('b', $c['a']);
$this->assertEquals('e', $c['c.d']);
$this->assertEquals('i', $c['h']);
$this->assertEquals('g', $c['c.f']);
//overwrite true, original values should be overwritten
$c = new DSO($data);
$c->merge([
'a' => 'B',
'c' => [
'd' => 'E',
'f' => 'g'
],
'h' => 'i'
], null, true);
$this->assertEquals('B', $c['a']);
$this->assertEquals('E', $c['c.d']);
$this->assertEquals('i', $c['h']);
$this->assertEquals('g', $c['c.f']);
//overwrite false with mismatched array-ness
$c = new DSO($data);
$c->merge([
'a' => ['b'=>'c'],
'c' => 'd'
]);
$this->assertEquals('b', $c['a']);
$this->assertEquals('e', $c['c.d']);
//overwrite true with mismatched array-ness
$c = new DSO($data);
$c->merge([
'a' => ['b'=>'c'],
'c' => 'd'
], null, true);
$this->assertEquals('c', $c['a.b']);
$this->assertEquals('d', $c['c']);
}
public function testConstructionUnflattening()
{
$arr = new DSO([
'foo.bar' => 'baz'
]);
$this->assertEquals(
['foo'=>['bar'=>'baz']],
$arr->get()
);
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Digraph\DataObject\Tests\DataTransformers;
use PHPUnit\Framework\TestCase;
use Digraph\DataObject\DataTransformers\JSON;
class JSONTest extends TestCase
{
public function testStorageValues()
{
$o = 'foo';
$h = new JSON($o);
$pre = $h->getStorageValue();
$this->assertEquals('[]', $pre);
$h['foo'] = 'bar';
$post = $h->getStorageValue();
$this->assertEquals(1, preg_match('/\{["\']foo["\']:["\']bar["\']\}/', $post));
unset($h['foo']);
$unset = $h->getStorageValue();
$this->assertEquals('[]', $unset);
}
public function testNullValues()
{
$o = 'foo';
$h = new JSON($o);
$pre = $h->getStorageValue();
$this->assertEquals('[]', $pre);
$h['foo'] = array(
'bar' => null,
'baz' => 'buzz'
);
$post = $h->getStorageValue();
$this->assertEquals(1, preg_match('/\{["\']foo["\']:{["\']baz["\']:["\']buzz["\']}\}/', $post));
}
public function testNullValuesOverwriting()
{
$o = 'foo';
$h = new JSON($o);
$pre = $h->getStorageValue();
$this->assertEquals('[]', $pre);
$h['foo'] = array(
'bar' => 'baz',
'baz' => 'buzz'
);
$h['foo'] = array(
'bar' => null,
'baz' => 'buzz'
);
$post = $h->getStorageValue();
$this->assertEquals(1, preg_match('/\{["\']foo["\']:{["\']baz["\']:["\']buzz["\']}\}/', $post));
}
}

View file

@ -1,191 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare (strict_types = 1);
namespace Destructr\Drivers;
use Destructr\Factory;
use PHPUnit\DbUnit\TestCaseTrait;
use PHPUnit\Framework\TestCase;
abstract class AbstractSQLDriverIntegrationTest extends TestCase
{
use TestCaseTrait;
const TEST_TABLE = 'integrationtest';
protected static function DRIVER_USERNAME()
{
return null;
}
protected static function DRIVER_PASSWORD()
{
return null;
}
protected static function DRIVER_OPTIONS()
{
return null;
}
public static function setUpBeforeClass()
{
$pdo = static::createPDO();
$pdo->exec('DROP TABLE ' . static::TEST_TABLE);
}
public function testPrepareEnvironment()
{
$factory = $this->createFactory();
$factory->prepareEnvironment();
//table should exist and have zero rows
$this->assertEquals(0, $this->getConnection()->getRowCount(static::TEST_TABLE));
}
public function testInsert()
{
$startRowCount = $this->getConnection()->getRowCount(static::TEST_TABLE);
$factory = $this->createFactory();
//inserting a freshly created object should return true
$o = $factory->create(['dso.id' => 'object-one']);
$this->assertTrue($o->insert());
//inserting it a second time should not
$this->assertFalse($o->insert());
//there should now be one more row
$this->assertEquals($startRowCount + 1, $this->getConnection()->getRowCount(static::TEST_TABLE));
}
public function testReadAndUpdate()
{
$startRowCount = $this->getConnection()->getRowCount(static::TEST_TABLE);
$factory = $this->createFactory();
//insert some new objects
$a1 = $factory->create(['foo' => 'bar']);
$a1->insert();
$b1 = $factory->create(['foo.bar' => 'baz']);
$b1->insert();
//read objects back out
$a2 = $factory->read($a1['dso.id']);
$b2 = $factory->read($b1['dso.id']);
//objects should be the same
$this->assertEquals($a1->get(), $a2->get());
$this->assertEquals($b1->get(), $b2->get());
//alter things in the objects and update them
$a2['foo'] = 'baz';
$b2['foo.bar'] = 'bar';
$a2->update();
$b2->update();
//read objects back out a third time
$a3 = $factory->read($a1['dso.id']);
$b3 = $factory->read($b1['dso.id']);
//objects should be the same
$this->assertEquals($a2->get(), $a3->get());
$this->assertEquals($b2->get(), $b3->get());
//they should not be the same as the originals from the beginning
$this->assertNotEquals($a1->get(), $a3->get());
$this->assertNotEquals($b1->get(), $b3->get());
//there should now be two more rows
$this->assertEquals($startRowCount + 2, $this->getConnection()->getRowCount(static::TEST_TABLE));
}
public function testDelete()
{
$startRowCount = $this->getConnection()->getRowCount(static::TEST_TABLE);
$factory = $this->createFactory();
//insert some new objects
$a1 = $factory->create(['testDelete' => 'undelete me']);
$a1->insert();
$b1 = $factory->create(['testDelete' => 'should be permanently deleted']);
$b1->insert();
//there should now be two more rows
$this->assertEquals($startRowCount + 2, $this->getConnection()->getRowCount(static::TEST_TABLE));
//delete one permanently and the other not, both shoudl take effect immediately
$a1->delete();
$b1->delete(true);
//there should now be only one more row
$this->assertEquals($startRowCount + 1, $this->getConnection()->getRowCount(static::TEST_TABLE));
//a should be possible to read a back out with the right flags
$this->assertNull($factory->read($a1['dso.id']));
$this->assertNotNull($factory->read($a1['dso.id'], 'dso.id', true));
$this->assertNotNull($factory->read($a1['dso.id'], 'dso.id', null));
//undelete a, should have update() inside it
$a1->undelete();
//it should be possible to read a back out with different flags
$this->assertNotNull($factory->read($a1['dso.id']));
$this->assertNull($factory->read($a1['dso.id'], 'dso.id', true));
$this->assertNotNull($factory->read($a1['dso.id'], 'dso.id', null));
}
public function testSearch()
{
$startRowCount = $this->getConnection()->getRowCount(static::TEST_TABLE);
$factory = $this->createFactory();
//insert some dummy data
$factory->create([
'testSearch' => 'a',
'a' => '1',
'b' => '2',
])->insert();
$factory->create([
'testSearch' => 'b',
'a' => '2',
'b' => '1',
])->insert();
$factory->create([
'testSearch' => 'c',
'a' => '3',
'b' => '4',
])->insert();
$factory->create([
'testSearch' => 'a',
'a' => '4',
'b' => '3',
])->insert();
//there should now be four more rows
$this->assertEquals($startRowCount + 4, $this->getConnection()->getRowCount(static::TEST_TABLE));
//TODO: test some searches
}
/**
* Creates a Driver from class constants, so extending classes can test
* different databases.
*/
public function createDriver()
{
$class = static::DRIVER_CLASS;
return new $class(
static::DRIVER_DSN(),
static::DRIVER_USERNAME(),
static::DRIVER_PASSWORD(),
static::DRIVER_OPTIONS()
);
}
public function createFactory()
{
$driver = $this->createDriver();
return new Factory(
$driver,
static::TEST_TABLE
);
}
protected static function createPDO()
{
return new \PDO(
static::DRIVER_DSN(),
static::DRIVER_USERNAME(),
static::DRIVER_PASSWORD(),
static::DRIVER_OPTIONS()
);
}
public function getConnection()
{
return $this->createDefaultDBConnection($this->createPDO(), 'phpunit');
}
public function getDataSet()
{
return new \PHPUnit\DbUnit\DataSet\DefaultDataSet();
}
}

View file

@ -1,154 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare (strict_types = 1);
namespace Destructr\Drivers;
use PDO;
use PHPUnit\DbUnit\TestCaseTrait;
use PHPUnit\Framework\TestCase;
/**
* This class tests a Driver's ability to correctly change schemas.
*/
abstract class AbstractSQLDriverSchemaChangeTest extends TestCase
{
use TestCaseTrait;
const TEST_TABLE = 'schematest';
protected static function DRIVER_USERNAME()
{
return null;
}
protected static function DRIVER_PASSWORD()
{
return null;
}
protected static function DRIVER_OPTIONS()
{
return null;
}
public function testSchemaChanges()
{
// set up using schema A
$factory = $this->createFactoryA();
$this->assertFalse($factory->checkEnvironment());
$factory->prepareEnvironment();
$this->assertTrue($factory->checkEnvironment());
$factory->updateEnvironment();
// verify schema in database
$this->assertEquals(
$factory->schema,
$factory->driver()->getSchema('schematest')
);
// add some content
$new = $factory->create([
'dso.id' => 'dso1',
'test.a' => 'value a1',
'test.b' => 'value b1',
'test.c' => 'value c1',
]);
$new->insert();
$new = $factory->create([
'dso.id' => 'dso2',
'test.a' => 'value a2',
'test.b' => 'value b2',
'test.c' => 'value c2',
]);
$new->insert();
$new = $factory->create([
'dso.id' => 'dso3',
'test.a' => 'value a3',
'test.b' => 'value b3',
'test.c' => 'value c3',
]);
$new->insert();
// verify data in table matches
$pdo = $this->createPDO();
$this->assertEquals(3, $this->getConnection()->getRowCount('schematest'));
for ($i = 1; $i <= 3; $i++) {
$row = $pdo->query('select dso_id, test_a, test_b from schematest where dso_id = "dso' . $i . '"')->fetch(PDO::FETCH_ASSOC);
$this->assertEquals(['dso_id' => "dso$i", 'test_a' => "value a$i", 'test_b' => "value b$i"], $row);
}
// change to schema B
sleep(1); //a table can't have its schema updated faster than once per second
$factory = $this->createFactoryB();
$this->assertFalse($factory->checkEnvironment());
$factory->prepareEnvironment();
$this->assertFalse($factory->checkEnvironment());
$factory->updateEnvironment();
$this->assertTrue($factory->checkEnvironment());
// verify schema in database
$this->assertEquals(
$factory->schema,
$factory->driver()->getSchema('schematest')
);
// verify data in table matches
$pdo = $this->createPDO();
$this->assertEquals(3, $this->getConnection()->getRowCount('schematest'));
for ($i = 1; $i <= 3; $i++) {
$row = $pdo->query('select dso_id, test_a_2, test_c from schematest where dso_id = "dso' . $i . '"')->fetch(PDO::FETCH_ASSOC);
$this->assertEquals(['dso_id' => "dso$i", 'test_a_2' => "value a$i", 'test_c' => "value c$i"], $row);
}
}
protected static function createFactoryA()
{
$driver = static::createDriver();
$factory = new FactorySchemaA(
$driver,
static::TEST_TABLE
);
return $factory;
}
protected static function createFactoryB()
{
$driver = static::createDriver();
$factory = new FactorySchemaB(
$driver,
static::TEST_TABLE
);
return $factory;
}
protected static function createDriver()
{
$class = static::DRIVER_CLASS;
return new $class(
static::DRIVER_DSN(),
static::DRIVER_USERNAME(),
static::DRIVER_PASSWORD(),
static::DRIVER_OPTIONS()
);
}
protected static function createPDO()
{
return new \PDO(
static::DRIVER_DSN(),
static::DRIVER_USERNAME(),
static::DRIVER_PASSWORD(),
static::DRIVER_OPTIONS()
);
}
public function getConnection()
{
return $this->createDefaultDBConnection($this->createPDO(), 'phpunit');
}
public function getDataSet()
{
return new \PHPUnit\DbUnit\DataSet\DefaultDataSet();
}
public static function setUpBeforeClass()
{
$pdo = static::createPDO();
$pdo->exec('DROP TABLE schematest');
$pdo->exec('DROP TABLE destructr_schema');
}
}

View file

@ -1,196 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers;
use Destructr\DSO;
use Destructr\Factory;
use Destructr\Search;
use PHPUnit\DbUnit\TestCaseTrait;
use PHPUnit\Framework\TestCase;
/**
* This class tests a factory in isolation. In the name of simplicity it's a bit
* simplistic, because it doesn't get the help of the Factory.
*
* There is also a class called AbstractSQLDriverIntegrationTest that tests drivers
* through a Factory. The results of that are harder to interpret, but more
* properly and thoroughly test the Drivers in a real environment.
*/
abstract class AbstractSQLDriverTest extends TestCase
{
use TestCaseTrait;
abstract protected static function DRIVER_DSN();
protected static function DRIVER_USERNAME()
{
return null;
}
protected static function DRIVER_PASSWORD()
{
return null;
}
protected static function DRIVER_OPTIONS()
{
return null;
}
/*
In actual practice, these would come from a Factory
*/
protected $schema = [
'dso.id' => [
'name' => 'dso_id',
'type' => 'VARCHAR(16)',
'index' => 'BTREE',
'unique' => true,
],
'dso.type' => [
'name' => 'dso_type',
'type' => 'VARCHAR(30)',
'index' => 'BTREE',
],
'dso.deleted' => [
'name' => 'dso_deleted',
'type' => 'BIGINT',
'index' => 'BTREE',
],
];
public function testPrepareEnvironment()
{
$driver = $this->createDriver();
$this->assertFalse($driver->tableExists('testPrepareEnvironment'));
$this->assertFalse($driver->tableExists(AbstractDriver::SCHEMA_TABLE));
$driver->prepareEnvironment('testPrepareEnvironment', $this->schema);
$this->assertTrue($driver->tableExists(AbstractDriver::SCHEMA_TABLE));
$this->assertTrue($driver->tableExists('testPrepareEnvironment'));
$this->assertEquals(1, $this->getConnection()->getRowCount(AbstractDriver::SCHEMA_TABLE));
$this->assertEquals(0, $this->getConnection()->getRowCount('testPrepareEnvironment'));
}
public function testInsert()
{
$driver = $this->createDriver();
$driver->prepareEnvironment('testInsert', $this->schema);
//test inserting an object
$o = new DSO(['dso.id' => 'first-inserted'], new Factory($driver, 'no_table'));
$this->assertTrue($driver->insert('testInsert', $o));
$this->assertEquals(1, $this->getConnection()->getRowCount('testInsert'));
//test inserting a second object
$o = new DSO(['dso.id' => 'second-inserted'], new Factory($driver, 'no_table'));
$this->assertTrue($driver->insert('testInsert', $o));
$this->assertEquals(2, $this->getConnection()->getRowCount('testInsert'));
}
public function testSelect()
{
$driver = $this->createDriver();
$driver->prepareEnvironment('testSelect', $this->schema);
//set up dummy data
$this->setup_testSelect();
//empty search
$search = new Search();
$results = $driver->select('testSelect', $search, []);
$this->assertSame(4, count($results));
//sorting by json value sort
$search = new Search();
$search->order('${sort} asc');
$results = $driver->select('testSelect', $search, []);
$this->assertSame(4, count($results));
$results = array_map(
function ($a) {
return json_decode($a['json_data'], true);
},
$results
);
$this->assertSame('item-a-1', $results[0]['dso']['id']);
$this->assertSame('item-b-1', $results[1]['dso']['id']);
$this->assertSame('item-a-2', $results[2]['dso']['id']);
$this->assertSame('item-b-2', $results[3]['dso']['id']);
// search with no results, searching by virtual column
$search = new Search();
$search->where('`dso_type` = :param');
$results = $driver->select('testSelect', $search, [':param' => 'type-none']);
$this->assertSame(0, count($results));
// search with no results, searching by json field
$search = new Search();
$search->where('${foo} = :param');
$results = $driver->select('testSelect', $search, [':param' => 'nonexistent foo value']);
$this->assertSame(0, count($results));
}
protected function setup_testSelect()
{
$driver = $this->createDriver();
$driver->insert('testSelect', new DSO([
'dso' => ['id' => 'item-a-1', 'type' => 'type-a'],
'foo' => 'bar',
'sort' => 'a',
], new Factory($driver, 'no_table')));
$driver->insert('testSelect', new DSO([
'dso' => ['id' => 'item-a-2', 'type' => 'type-a'],
'foo' => 'baz',
'sort' => 'c',
], new Factory($driver, 'no_table')));
$driver->insert('testSelect', new DSO([
'dso' => ['id' => 'item-b-1', 'type' => 'type-b'],
'foo' => 'buz',
'sort' => 'b',
], new Factory($driver, 'no_table')));
$driver->insert('testSelect', new DSO([
'dso' => ['id' => 'item-b-2', 'type' => 'type-b', 'deleted' => 100],
'foo' => 'quz',
'sort' => 'd',
], new Factory($driver, 'no_table')));
}
/**
* Creates a Driver from class constants, so extending classes can test
* different databases.
*/
public function createDriver()
{
$class = static::DRIVER_CLASS;
return new $class(
static::DRIVER_DSN(),
static::DRIVER_USERNAME(),
static::DRIVER_PASSWORD(),
static::DRIVER_OPTIONS()
);
}
public static function setUpBeforeClass()
{
$pdo = static::createPDO();
$pdo->exec('DROP TABLE testPrepareEnvironment');
$pdo->exec('DROP TABLE testInsert');
$pdo->exec('DROP TABLE testSelect');
$pdo->exec('DROP TABLE destructr_schema');
}
protected static function createPDO()
{
return new \PDO(
static::DRIVER_DSN(),
static::DRIVER_USERNAME(),
static::DRIVER_PASSWORD(),
static::DRIVER_OPTIONS()
);
}
public function getConnection()
{
return $this->createDefaultDBConnection($this->createPDO(), 'phpunit');
}
public function getDataSet()
{
return new \PHPUnit\DbUnit\DataSet\DefaultDataSet();
}
}

View file

@ -1,30 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare (strict_types = 1);
namespace Destructr\Drivers;
use Destructr\Factory;
class FactorySchemaA extends Factory
{
public $schema = [
'dso.id' => [
'name' => 'dso_id', //column name to be used
'type' => 'VARCHAR(16)', //column type
'index' => 'BTREE', //whether/how to index
'unique' => true, //whether column should be unique
'primary' => true, //whether column should be the primary key
],
'test.a' => [
'name' => 'test_a',
'type' => 'VARCHAR(100)',
'index' => 'BTREE',
]
,
'test.b' => [
'name' => 'test_b',
'type' => 'VARCHAR(100)',
'index' => 'BTREE',
],
];
}

View file

@ -1,30 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare (strict_types = 1);
namespace Destructr\Drivers;
use Destructr\Factory;
class FactorySchemaB extends Factory
{
public $schema = [
'dso.id' => [
'name' => 'dso_id', //column name to be used
'type' => 'VARCHAR(16)', //column type
'index' => 'BTREE', //whether/how to index
'unique' => true, //whether column should be unique
'primary' => true, //whether column should be the primary key
],
'test.a' => [
'name' => 'test_a_2',
'type' => 'VARCHAR(100)',
'index' => 'BTREE',
]
,
'test.c' => [
'name' => 'test_c',
'type' => 'VARCHAR(100)',
'index' => 'BTREE',
],
];
}

View file

@ -1,39 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MariaDB;
use Destructr\Drivers\AbstractSQLDriverIntegrationTest;
use Destructr\Drivers\MariaDBDriver;
class MariaDBDriverIntegrationTest extends AbstractSQLDriverIntegrationTest
{
const DRIVER_CLASS = MariaDBDriver::class;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MARIADB_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MARIADB_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MARIADB_PASSWORD'] ?? 'root';
}
}

View file

@ -1,39 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MariaDB;
use Destructr\Drivers\AbstractSQLDriverSchemaChangeTest;
use Destructr\Drivers\MariaDBDriver;
class MariaDBDriverSchemaChangeTest extends AbstractSQLDriverSchemaChangeTest
{
const DRIVER_CLASS = MariaDBDriver::class;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MARIADB_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MARIADB_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MARIADB_PASSWORD'] ?? 'root';
}
}

View file

@ -1,39 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MariaDB;
use Destructr\Drivers\AbstractSQLDriverTest;
use Destructr\Drivers\MariaDBDriver;
class MariaDBDriverTest extends AbstractSQLDriverTest
{
const DRIVER_CLASS = MariaDBDriver::class;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MARIADB_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MARIADB_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MARIADB_PASSWORD'] ?? 'root';
}
}

View file

@ -1,39 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MySQL;
use Destructr\Drivers\AbstractSQLDriverIntegrationTest;
use Destructr\Drivers\MySQLDriver;
class MySQLDriverIntegrationTest extends AbstractSQLDriverIntegrationTest
{
const DRIVER_CLASS = MySQLDriver::class;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MYSQL_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MYSQL_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MYSQL_PASSWORD'] ?? 'root';
}
}

View file

@ -1,39 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MySQL;
use Destructr\Drivers\AbstractSQLDriverSchemaChangeTest;
use Destructr\Drivers\MySQLDriver;
class MySQLDriverSchemaChangeTest extends AbstractSQLDriverSchemaChangeTest
{
const DRIVER_CLASS = MySQLDriver::class;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MYSQL_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MYSQL_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MYSQL_PASSWORD'] ?? 'root';
}
}

View file

@ -1,39 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MySQL;
use Destructr\Drivers\AbstractSQLDriverTest;
use Destructr\Drivers\MySQLDriver;
class MySQLDriverTest extends AbstractSQLDriverTest
{
const DRIVER_CLASS = MySQLDriver::class;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MYSQL_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MYSQL_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MYSQL_PASSWORD'] ?? 'root';
}
}

View file

@ -1,34 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\SQLite;
use Destructr\Drivers\AbstractSQLDriverIntegrationTest;
use Destructr\Drivers\SQLiteDriver;
class SQLiteDriverIntegrationTest extends AbstractSQLDriverIntegrationTest
{
const DRIVER_CLASS = SQLiteDriver::class;
public static function setUpBeforeClass()
{
@unlink(__DIR__ . '/integration.test.sqlite');
}
public static function DRIVER_DSN()
{
return 'sqlite:' . __DIR__ . '/integration.test.sqlite';
}
protected static function DRIVER_DBNAME()
{
return null;
}
protected static function DRIVER_USERNAME()
{
return null;
}
}

View file

@ -1,34 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\SQLite;
use Destructr\Drivers\AbstractSQLDriverSchemaChangeTest;
use Destructr\Drivers\SQLiteDriver;
class SQLiteDriverSchemaChangeTest extends AbstractSQLDriverSchemaChangeTest
{
const DRIVER_CLASS = SQLiteDriver::class;
public static function setUpBeforeClass()
{
@unlink(__DIR__ . '/schema.test.sqlite');
}
public static function DRIVER_DSN()
{
return 'sqlite:' . __DIR__ . '/schema.test.sqlite';
}
protected static function DRIVER_DBNAME()
{
return null;
}
protected static function DRIVER_USERNAME()
{
return null;
}
}

View file

@ -1,34 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\SQLite;
use Destructr\Drivers\AbstractSQLDriverTest;
use Destructr\Drivers\SQLiteDriver;
class SQLiteDriverTest extends AbstractSQLDriverTest
{
const DRIVER_CLASS = SQLiteDriver::class;
public static function setUpBeforeClass()
{
@unlink(__DIR__ . '/driver.test.sqlite');
}
public static function DRIVER_DSN()
{
return 'sqlite:' . __DIR__ . '/driver.test.sqlite';
}
protected static function DRIVER_DBNAME()
{
return null;
}
protected static function DRIVER_USERNAME()
{
return null;
}
}

View file

@ -1,112 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr;
use PHPUnit\Framework\TestCase;
class FactoryTest extends TestCase
{
public function testSearch()
{
$d = new HarnessDriver('testSearch');
$f = new Factory($d, 'table_name');
$s = $f->search();
$s->where('foo = bar');
//default execute, should be adding dso_deleted is null to where
$s->execute(['p'=>'q']);
$this->assertEquals('table_name', $d->last_select['table']);
$this->assertEquals('(foo = bar) AND `dso_deleted` is null', $d->last_select['search']->where());
$this->assertEquals(['p'=>'q'], $d->last_select['params']);
//execute with deleted=true, should be adding dso_deleted is not null to where
$s->execute(['p'=>'q'], true);
$this->assertEquals('table_name', $d->last_select['table']);
$this->assertEquals('(foo = bar) AND `dso_deleted` is not null', $d->last_select['search']->where());
$this->assertEquals(['p'=>'q'], $d->last_select['params']);
//execute with deleted=null, shouldn't touch where
$s->execute(['p'=>'q'], null);
$this->assertEquals('table_name', $d->last_select['table']);
$this->assertEquals('foo = bar', $d->last_select['search']->where());
$this->assertEquals(['p'=>'q'], $d->last_select['params']);
}
public function testInsert()
{
$d = new HarnessDriver('testInsert');
$f = new Factory($d, 'table_name');
//creating a new object
$o = $f->create();
$o->insert();
$this->assertEquals('table_name', $d->last_insert['table']);
$this->assertEquals($o->get('dso.id'), $d->last_insert['dso']->get('dso.id'));
//creating a second object to verify
$o = $f->create();
$o->insert();
$this->assertEquals('table_name', $d->last_insert['table']);
$this->assertEquals($o->get('dso.id'), $d->last_insert['dso']->get('dso.id'));
}
public function testUpdate()
{
$d = new HarnessDriver('testUpdate');
$f = new Factory($d, 'table_name');
//creatingtwo new objects
$o1 = $f->create(['dso.id'=>'object1id']);
$o2 = $f->create(['dso.id'=>'object2id']);
//initially, updating shouldn't do anything because there are no changes
$o1->update();
$this->assertNull($d->last_update);
//after making changes updating should do something
$o1['foo'] = 'bar';
$o1->update();
$this->assertEquals('table_name', $d->last_update['table']);
$this->assertEquals($o1->get('dso.id'), $d->last_update['dso']->get('dso.id'));
//updating second object to verify
$o2['foo'] = 'bar';
$o2->update();
$this->assertEquals('table_name', $d->last_update['table']);
$this->assertEquals($o2->get('dso.id'), $d->last_update['dso']->get('dso.id'));
//calling update on first object shouldn't do anything, because it hasn't changed
$o1->update();
$this->assertEquals('table_name', $d->last_update['table']);
$this->assertEquals($o2->get('dso.id'), $d->last_update['dso']->get('dso.id'));
//after making changes updating should do something
$o1['foo'] = 'baz';
$o1->update();
$this->assertEquals('table_name', $d->last_update['table']);
$this->assertEquals($o1->get('dso.id'), $d->last_update['dso']->get('dso.id'));
}
public function testDelete()
{
$d = new HarnessDriver('testInsert');
$f = new Factory($d, 'table_name');
//non-permanent delete shouldn't do anything with deletion, but should call update
$o = $f->create();
$o->delete();
$this->assertNull($d->last_delete);
$this->assertEquals('table_name', $d->last_update['table']);
$this->assertEquals($o->get('dso.id'), $d->last_update['dso']->get('dso.id'));
//undelete should also not engage delete, but should call update
$d->last_update = null;
$o->undelete();
$this->assertNull($d->last_delete);
$this->assertEquals('table_name', $d->last_update['table']);
$this->assertEquals($o->get('dso.id'), $d->last_update['dso']->get('dso.id'));
//non-permanent delete shouldn't do anything with update, but should call delete
$d->last_update = null;
$o = $f->create();
$o->delete(true);
$this->assertNull($d->last_update);
$this->assertEquals('table_name', $d->last_delete['table']);
$this->assertEquals($o->get('dso.id'), $d->last_delete['dso']->get('dso.id'));
}
}

View file

@ -1,69 +0,0 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr;
class HarnessDriver extends Drivers\AbstractDriver
{
const EXTENSIBLE_VIRTUAL_COLUMNS = true;
public $last_select;
public $last_insert;
public $last_update;
public $last_delete;
public function __construct(string $dsn=null, string $username=null, string $password=null, array $options=null)
{
}
public function pdo(\PDO $pdo=null) : ?\PDO {
return null;
}
public function prepareEnvironment(string $table, array $schema) : bool
{
//TODO: add tests for this too
return false;
}
public function select(string $table, Search $search, array $params) : array
{
$this->last_select = [
'table' => $table,
'search' => $search,
'params' => $params
];
return [];
}
public function insert(string $table, DSOInterface $dso) : bool
{
$this->dsn = 'inserting';
$this->last_insert = [
'table' => $table,
'dso' => $dso
];
return true;
}
public function update(string $table, DSOInterface $dso) : bool
{
$this->last_update = [
'table' => $table,
'dso' => $dso
];
return true;
}
public function delete(string $table, DSOInterface $dso) : bool
{
$this->last_delete = [
'table' => $table,
'dso' => $dso
];
return true;
}
public function errorInfo()
{
return [];
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Digraph\DataObject\Tests\Utils;
use PHPUnit\Framework\TestCase;
use Digraph\DataObject\Utils\BadWords;
class BadWordsTest extends TestCase
{
public function testProfanity()
{
$this->assertTrue(BadWords::profane('fuck'));
$this->assertTrue(BadWords::profane('phuxx'));
$this->assertFalse(BadWords::profane('ABCD'));
$this->assertTrue(BadWords::profane('secks'));
}
}