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
58 changed files with 2571 additions and 2129 deletions

10
.gitignore vendored
View file

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

View file

@ -1,28 +0,0 @@
image: php:7.1-alpine
services:
- mysql:5.7
variables:
MYSQL_DATABASE: destructr_test
MYSQL_ROOT_PASSWORD: badpassword
before_script:
- apk update
- apk add git
- docker-php-ext-install pdo
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install
test:local:
script:
- php composer.phar test-local
test:mysql:
script:
- docker-php-ext-install pdo_mysql
- php composer.phar test-mysql
test:sqlite:
script:
- php composer.phar test-sqlite

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

View file

@ -1,82 +1,32 @@
# Destructr
# Digraph DataObject v0.5
Destructr is a specialized ORM that allows a seamless mix of structured, relational data with unstructured JSON data.
[![Build Status](https://travis-ci.org/digraphcms/digraph-dataobject.svg?branch=v0.5)](https://travis-ci.org/digraphcms/digraph-dataobject)
## Getting started
*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.
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.
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.
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.
Also this version will probably never be properly documented.
### Database driver and factory
## MIT License
In order to read/write objects from a database table, you'll need to configure a Driver and Factory class.
Copyright (c) 2017 Joby Elliott <joby@byjoby.com>
```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
);
```
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:
### Creating a new record
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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);
```
## 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.
Exact requirements are in flux during development, but basically if a database doesn't have JSON functions it's probably impossible for Destructr to ever work with it.
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
* MariaDB >=10.2
* 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.
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,41 +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",
"mofodojodino/profanity-filter": "^1.3",
"byjoby/flatrr": "^1"
},
"require-dev": {
"phpunit/phpunit": "^7",
"phpunit/dbunit": "^4"
},
"scripts": {
"test": [
"phpunit"
],
"test-local": [
"phpunit --testsuite Local"
],
"test-mysql": [
"phpunit --testsuite MySQL"
],
"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,15 +1,7 @@
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="Local">
<testsuite name="Digraph Tests">
<directory>tests</directory>
<exclude>tests/Drivers</exclude>
<exclude>tests/LegacyDrivers</exclude>
</testsuite>
<testsuite name="MySQL">
<directory>tests/Drivers/MySQL</directory>
</testsuite>
<testsuite name="SQLite">
<directory>tests/LegacyDrivers/SQLite</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,121 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr;
use \Flatrr\FlatArray;
/**
* Interface for DeStructure 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, DSOFactoryInterface &$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
{
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 = null, $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);
}
}
public 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(DSOFactoryInterface &$factory = null) : ?DSOFactoryInterface
{
if ($factory) {
$this->factory = $factory;
}
return $this->factory;
}
}

View file

@ -1,20 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr;
interface DSOFactoryInterface
{
public function __construct(Drivers\DSODriverInterface &$driver, string $table);
public function class(array $data) : ?string;
public function createTable() : bool;
public function create(array $data = array()) : DSOInterface;
public function read(string $value, string $field = 'dso.id', $deleted = false) : ?DSOInterface;
public function insert(DSOInterface &$dso) : bool;
public function update(DSOInterface &$dso) : bool;
public function delete(DSOInterface &$dso, bool $permanent = false) : bool;
public function search() : Search;
public function executeSearch(Search $search, array $params = array(), $deleted = false) : array;
}

View file

@ -1,27 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/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, DSOFactoryInterface &$factory = null);
public function factory(DSOFactoryInterface &$factory = null) : ?DSOFactoryInterface;
public function set(string $name = null, $value, $force=false);
public function resetChanges();
public function changes() : array;
public function removals() : array;
public function insert() : bool;
public function update() : 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,24 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr;
class DriverFactory
{
public static $map = [
'mysql' => Drivers\MySQLDriver::class,
'sqlite' => LegacyDrivers\SQLiteDriver::class
];
public static function factory(string $dsn, string $username=null, string $password=null, array $options=null, string $type = null) : ?Drivers\DSODriverInterface
{
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;
}
}
}

View file

@ -1,120 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr\Drivers;
use Destructr\DSOInterface;
use Destructr\Search;
//TODO: Caching? It should happen somewhere in this class I think.
abstract class AbstractDriver implements DSODriverInterface
{
public $lastPreparationErrorOn;
public $pdo;
const EXTENSIBLE_VIRTUAL_COLUMNS = true;
public function __construct(string $dsn, string $username=null, string $password=null, array $options=null)
{
if (!$this->pdo = new \PDO($dsn, $username, $password, $options)) {
throw new \Exception("Error creating PDO connection");
}
}
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 createTable(string $table, array $virtualColumns) : bool
{
$sql = $this->sql_ddl([
'table'=>$table,
'virtualColumns'=>$virtualColumns
]);
return $this->pdo->exec($sql) !== false;
}
public function update(string $table, DSOInterface $dso) : bool
{
if (!$dso->changes() && !$dso->removals()) {
return true;
}
$s = $this->getStatement(
'setJSON',
['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 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;
//TODO: turn this on someday and see if caching statements helps in the real world
// $sql = $this->$fn($args);
// $id = md5($sql);
// if (!isset($this->prepared[$id])) {
// $this->prepared[$id] = $this->pdo->prepare($sql);
// }
// return @$this->prepared[$id];
}
}

View file

@ -1,19 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr\Drivers;
use Destructr\DSOInterface;
use Destructr\Search;
interface DSODriverInterface
{
public function __construct(string $dsn, string $username=null, string $password=null, array $options=null);
public function createTable(string $table, array $virtualColumns) : bool;
public function select(string $table, Search $search, array $params);
public function insert(string $table, DSOInterface $dso) : bool;
public function update(string $table, DSOInterface $dso) : bool;
public function delete(string $table, DSOInterface $dso) : bool;
public function errorInfo();
}

View file

@ -1,97 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr\Drivers;
/**
* What this driver supports: MySQL and MariaDB databases new enough to support
* JSON functions. This means:
* * MySQL >= 5.7
* * MariaDB >= 10.2
*/
class MySQLDriver extends AbstractDriver
{
/**
* 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($args)
{
//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_ddl($args=array())
{
$out = [];
$out[] = "CREATE TABLE `{$args['table']}` (";
$lines = [];
$lines[] = "`json_data` JSON DEFAULT NULL";
foreach ($args['virtualColumns'] as $path => $col) {
$line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (".$this->expandPath($path).")";
if (@$col['primary']) {
//this needs to be "PERSISTENT" for MariaDB -- I guess there are going to be two drivers now
$line .= ' STORED';
} else {
$line .= ' VIRTUAL';
}
$lines[] = $line;
}
foreach ($args['virtualColumns'] as $path => $col) {
if (@$col['primary']) {
$lines[] = "PRIMARY KEY (`{$col['name']}`)";
} elseif (@$col['unique'] && $as = @$col['index']) {
$lines[] = "UNIQUE KEY `{$args['table']}_{$col['name']}_idx` (`{$col['name']}`) USING $as";
} elseif ($as = @$col['index']) {
$lines[] = "KEY `{$args['table']}_{$col['name']}_idx` (`{$col['name']}`) USING $as";
}
}
$out[] = implode(','.PHP_EOL, $lines);
$out[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8;";
return implode(PHP_EOL, $out);
}
protected function expandPath(string $path) : string
{
return "JSON_UNQUOTE(JSON_EXTRACT(`json_data`,'$.{$path}'))";
}
protected function sql_setJSON($args)
{
return 'UPDATE `'.$args['table'].'` SET `json_data` = :data WHERE `dso_id` = :dso_id;';
}
protected function sql_insert($args)
{
return "INSERT INTO `{$args['table']}` (`json_data`) VALUES (:data);";
}
protected function sql_delete($args)
{
return 'DELETE FROM `'.$args['table'].'` WHERE `dso_id` = :dso_id;';
}
}

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,275 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr;
use mofodojodino\ProfanityFilter\Check;
/**
* 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 implements DSOFactoryInterface
{
const ID_CHARS = 'abcdefghijkmnorstuvwxyz0123456789';
const ID_LENGTH = 16;
protected $driver;
protected $table;
/**
* Virtual columns are only supported by modern SQL servers. Most of the
* legacy drivers will only use the ones defined in CORE_VIRTUAL_COLUMNS,
* but that should be handled automatically.
*/
protected $virtualColumns = [
'dso.id' => [
'name'=>'dso_id',
'type'=>'VARCHAR(16)',
'index' => 'BTREE',
'unique' => true,
'primary' => true
],
'dso.type' => [
'name'=>'dso_type',
'type'=>'VARCHAR(30)',
'index'=>'BTREE'
],
'dso.deleted' => [
'name'=>'dso_deleted',
'type'=>'BIGINT',
'index'=>'BTREE'
]
];
/**
* This cannot be modified by extending classes, it's used by legacy drivers
*/
const CORE_VIRTUAL_COLUMNS = [
'dso.id' => [
'name'=>'dso_id',
'type'=>'VARCHAR(16)',
'index' => 'BTREE',
'unique' => true,
'primary' => true
],
'dso.type' => [
'name'=>'dso_type',
'type'=>'VARCHAR(30)',
'index'=>'BTREE'
],
'dso.deleted' => [
'name'=>'dso_deleted',
'type'=>'BIGINT',
'index'=>'BTREE'
]
];
public function __construct(Drivers\DSODriverInterface &$driver, string $table)
{
$this->driver = $driver;
$this->table = $table;
}
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']]);
}
public 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);
}
public function undelete(DSOInterface &$dso) : bool
{
unset($dso['dso.deleted']);
return $this->update($dso);
}
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 createTable() : bool
{
return $this->driver->createTable(
$this->table,
($this->driver::EXTENSIBLE_VIRTUAL_COLUMNS?$this->virtualColumns:$this::CORE_VIRTUAL_COLUMNS)
);
}
protected function virtualColumnName($path) : ?string
{
if ($this->driver::EXTENSIBLE_VIRTUAL_COLUMNS) {
$vcols = $this->virtualColumns;
} else {
$vcols = static::CORE_VIRTUAL_COLUMNS;
}
return @$vcols[$path]['name'];
}
public function update(DSOInterface &$dso) : bool
{
if (!$dso->changes() && !$dso->removals()) {
return true;
}
$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 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());
/* 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
{
$check = new Check();
do {
$id = '';
while (strlen($id) < $length) {
$id .= substr(
$chars,
rand(0, strlen($chars)-1),
1
);
}
} while ($check->hasProfanity($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

@ -1,183 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr\LegacyDrivers;
use Destructr\Drivers\AbstractDriver;
use Destructr\DSOInterface;
use Destructr\Factory;
use Destructr\Search;
use Flatrr\FlatArray;
/**
* This driver is for supporting older SQL servers that don't have their own
* JSON functions. It uses a highly suspect alternative JSON serialization and
* user-defined function.
*
* There are also DEFINITELY bugs in legacy drivers. They should only be used
* for very simple queries. These are probably also bugs that cannot be fixed.
* Legacy drivers shouldn't really be considered "supported" per se.
*/
class AbstractLegacyDriver extends AbstractDriver
{
const EXTENSIBLE_VIRTUAL_COLUMNS = false;
public function select(string $table, Search $search, array $params)
{
$results = parent::select($table, $search, $params);
foreach ($results as $rkey => $row) {
$new = new FlatArray();
foreach (json_decode($row['json_data'], true) as $key => $value) {
$new->set(str_replace('|', '.', $key), $value);
}
$results[$rkey]['json_data'] = json_encode($new->get());
}
return $results;
}
protected function sql_select($args)
{
//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_ddl($args=array())
{
$out = [];
$out[] = "CREATE TABLE `{$args['table']}` (";
$lines = [];
$lines[] = "`json_data` TEXT DEFAULT NULL";
foreach ($args['virtualColumns'] as $path => $col) {
$lines[] = "`{$col['name']}` {$col['type']}";
}
foreach ($args['virtualColumns'] as $path => $col) {
if (@$col['unique'] && $as = @$col['index']) {
$lines[] = "UNIQUE KEY `{$args['table']}_{$col['name']}_idx` (`{$col['name']}`) USING $as";
} elseif ($as = @$col['index']) {
$lines[] = "KEY `{$args['table']}_{$col['name']}_idx` (`{$col['name']}`) USING $as";
}
}
$out[] = implode(','.PHP_EOL, $lines);
$out[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8;";
return implode(PHP_EOL, $out);
}
public function update(string $table, DSOInterface $dso) : bool
{
if (!$dso->changes() && !$dso->removals()) {
return true;
}
$s = $this->getStatement(
'setJSON',
['table'=>$table]
);
$params = $this->legacyParams($dso);
$out = $s->execute($params);
return $out;
}
protected function sql_setJSON($args)
{
$out = [];
$out[] = 'UPDATE `'.$args['table'].'`';
$out[] = 'SET';
foreach (Factory::CORE_VIRTUAL_COLUMNS as $v) {
$out[] = '`'.$v['name'].'` = :'.$v['name'].',';
}
$out[] = '`json_data` = :data';
$out[] = 'WHERE `dso_id` = :dso_id';
return implode(PHP_EOL, $out).';';
}
public function insert(string $table, DSOInterface $dso) : bool
{
$s = $this->getStatement(
'insert',
['table'=>$table]
);
$params = $this->legacyParams($dso);
return $s->execute($params);
}
protected function legacyParams(DSOInterface $dso)
{
$params = [':data' => $this->json_encode($dso->get())];
foreach (Factory::CORE_VIRTUAL_COLUMNS as $vk => $vv) {
$params[':'.$vv['name']] = $dso->get($vk);
}
return $params;
}
protected function sql_insert($args)
{
$out = [];
$out[] = 'INSERT INTO `'.$args['table'].'`';
$out[] = '(`json_data`,`dso_id`,`dso_type`,`dso_deleted`)';
$out[] = 'VALUES (:data, :dso_id, :dso_type, :dso_deleted)';
return implode(PHP_EOL, $out).';';
}
public function delete(string $table, DSOInterface $dso) : bool
{
$s = $this->getStatement(
'delete',
['table'=>$table]
);
$out = $s->execute([
':dso_id' => $dso['dso.id']
]);
// if (!$out) {
// var_dump($s->errorInfo());
// }
return $out;
}
protected function sql_delete($args)
{
return 'DELETE FROM `'.$args['table'].'` WHERE `dso_id` = :dso_id;';
}
public function json_encode($a, array &$b = null, string $prefix = '')
{
if ($b === null) {
$b = [];
$this->json_encode($a, $b, '');
return json_encode($b);
} else {
if (is_array($a)) {
foreach ($a as $ak => $av) {
if ($prefix == '') {
$nprefix = $ak;
} else {
$nprefix = $prefix.'|'.$ak;
}
$this->json_encode($av, $b, $nprefix);
}
} else {
$b[$prefix] = $a;
}
}
}
}

View file

@ -1,45 +0,0 @@
# Destructr legacy drivers
This folder holds what are called "legacy drivers."
These drivers attempt to extend Destructr support to as many databases as possible,
even in some cases at the expense of functionality.
All these drivers are designed to support the following features:
* Inserting objects (and disallowing duplicates at the *SQL* level)
* Updating existing objects
* Deleting objects
* Searching by exact text matches
The things many of this sort of driver will *not* ever support:
* Performance optimization via virtual columns. Many of them will have their
performance lightly optimized for common system queries, just by hard-coding
virtual columns for `dso.id`, `dso.deleted`, and `dso.type`
* Complex queries, like joins, REGEX, or LIKE queries.
## Current state of legacy drivers
### SQLite 3
**\Destructr\LegacyDrivers\SQLiteDriver**
**Overall support level: Highly functional, a touch slow**
Via LegacyDrivers\SQLiteDriver Destructr now has surprisingly good support for
SQLite 3. It accomplishes this by using SQLite's user-defined functions feature
to offload JSON parsing to PHP. This means all the JSON parsing is actually up
to spec, storage doesn't need to be flattened like the other legacy drivers,
and tables are dramatically more easily forward-compatible.
If you're considering using legacy MySQL, you should really seriously consider
just using SQLite instead. In benchmarks with 1000 records SQLite's performance
is actually BETTER than MySQL 5.6 for everything but insert/update operations.
In some cases it appears to even be *significantly* faster while also having the
distinct advantage of not using any goofy home-rolled JSON extraction funcitons.
So unless you have a good reason to use MySQL 5.6, you're probably best off
using SQLite if you don't have access to a fully supported database version.
### MySQL 5.6
** No longer supported. You should really use SQLite if you don't have access to
something better. Will most likely never be supported. **

View file

@ -1,97 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr\LegacyDrivers;
use Destructr\DSOInterface;
use Destructr\Factory;
/**
* What this driver supports: Version of SQLite3 in PHP environments that allow
* pdo::sqliteCreateFunction
*
* Overally this driver is quite safe and reliable. Definitely the most safe of
* all the legacy drivers. The performance isn't even that bad. It benchmarks
* close to the same speed as MySQL 5.7, even. The benchmarks are only operating
* on databases of 500 objects though, so ymmv.
*/
class SQLiteDriver extends AbstractLegacyDriver
{
public function __construct(string $dsn, string $username=null, string $password=null, array $options=null)
{
parent::__construct($dsn, $username, $password, $options);
/*
What we're doing here is adding a custom function to SQLite so that it
can extract JSON values. It's not fast, but it does let us use JSON
fairly seamlessly.
*/
$this->pdo->sqliteCreateFunction(
'DESTRUCTR_JSON_EXTRACT',
'\\Destructr\\LegacyDrivers\\SQLiteDriver::JSON_EXTRACT',
2
);
}
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";
}
public function createTable(string $table, array $virtualColumns) : bool
{
$sql = $this->sql_ddl([
'table'=>$table,
]);
$out = $this->pdo->exec($sql) !== false;
foreach (Factory::CORE_VIRTUAL_COLUMNS as $key => $vcol) {
$idxResult = true;
if (@$vcol['unique']) {
$idxResult = $this->pdo->exec('CREATE UNIQUE INDEX '.$table.'_'.$vcol['name'].'_idx on `'.$table.'`(`'.$vcol['name'].'`)') !== false;
} elseif (@$vcol['index']) {
$idxResult = $this->pdo->exec('CREATE INDEX '.$table.'_'.$vcol['name'].'_idx on `'.$table.'`(`'.$vcol['name'].'`)') !== false;
}
if (!$idxResult) {
$out = false;
}
}
return $out;
}
protected function sql_ddl($args=array())
{
$out = [];
$out[] = "CREATE TABLE `{$args['table']}` (";
$lines = [];
$lines[] = "`json_data` TEXT DEFAULT NULL";
foreach (Factory::CORE_VIRTUAL_COLUMNS as $path => $col) {
$line = "`{$col['name']}` {$col['type']}";
if (@$col['primary']) {
$line .= ' PRIMARY KEY';
}
$lines[] = $line;
}
$out[] = implode(','.PHP_EOL, $lines);
$out[] = ");";
return implode(PHP_EOL, $out);
}
protected function expandPath(string $path) : string
{
return "DESTRUCTR_JSON_EXTRACT(`json_data`,'$.{$path}')";
}
public function json_encode($a, ?array &$b = null, string $prefix = '')
{
return json_encode($a);
}
}

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,73 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr;
use Destructr\DSOFactoryInterface;
use Destructr\Drivers\DSODriverInterface;
class Search implements \Serializable
{
protected $factory;
protected $where;
protected $order;
protected $limit;
protected $offset;
public function __construct(DSOFactoryInterface &$factory=null)
{
$this->factory = $factory;
}
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 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://gitlab.com/byjoby/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,184 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers;
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;
use Destructr\DSO;
use Destructr\Search;
use Destructr\Factory;
abstract class AbstractDriverIntegrationTest extends TestCase
{
use TestCaseTrait;
public static function setUpBeforeClass()
{
$pdo = static::createPDO();
$pdo->exec('DROP TABLE '.static::TEST_TABLE);
}
public function testCreateTable()
{
$factory = $this->createFactory();
//should work the first time
$out = $factory->createTable();
if (!$out) {
var_dump($factory->errorInfo());
}
$this->assertTrue($out);
//but not the second time, because it already exists
$this->assertFalse($factory->createTable());
//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,218 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers;
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;
use Destructr\DSO;
use Destructr\Search;
/**
* 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 AbstractDriverIntegrationTest 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 AbstractDriverTest extends TestCase
{
use TestCaseTrait;
/*
In actual practice, these would come from a Factory
*/
protected $virtualColumns = [
'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 testCreateTable()
{
$driver = $this->createDriver();
$res = $driver->createTable('testCreateTable', $this->virtualColumns);
$this->assertTrue($res);
$this->assertEquals(0, $this->getConnection()->getRowCount('testCreateTable'));
$this->assertFalse($driver->createTable('testCreateTable', $this->virtualColumns));
}
public function testInsert()
{
$driver = $this->createDriver();
$driver->createTable('testInsert', $this->virtualColumns);
//test inserting an object
$o = new DSO(['dso.id'=>'first-inserted']);
$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']);
$this->assertTrue($driver->insert('testInsert', $o));
$this->assertEquals(2, $this->getConnection()->getRowCount('testInsert'));
//test inserting a second object with an existing id, it shouldn't work
$o = new DSO(['dso.id'=>'first-inserted']);
$this->assertFalse($driver->insert('testInsert', $o));
$this->assertEquals(2, $this->getConnection()->getRowCount('testInsert'));
}
public function testSelect()
{
$driver = $this->createDriver();
$driver->createTable('testSelect', $this->virtualColumns);
//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));
}
public function testDelete()
{
$driver = $this->createDriver();
$driver->createTable('testDelete', $this->virtualColumns);
//set up dummy data
$this->setup_testDelete();
//try deleting an item
$dso = new DSO(['dso.id'=>'item-a-1']);
$driver->delete('testDelete', $dso);
$this->assertEquals(3, $this->getConnection()->getRowCount('testDelete'));
//try deleting an item at the other end of the table
$dso = new DSO(['dso.id'=>'item-b-2']);
$driver->delete('testDelete', $dso);
$this->assertEquals(2, $this->getConnection()->getRowCount('testDelete'));
}
protected function setup_testDelete()
{
$driver = $this->createDriver();
$driver->insert('testDelete', new DSO([
'dso'=>['id'=>'item-a-1','type'=>'type-a'],
'foo'=>'bar',
'sort'=>'a'
]));
$driver->insert('testDelete', new DSO([
'dso'=>['id'=>'item-a-2','type'=>'type-a'],
'foo'=>'baz',
'sort'=>'c'
]));
$driver->insert('testDelete', new DSO([
'dso'=>['id'=>'item-b-1','type'=>'type-b'],
'foo'=>'buz',
'sort'=>'b'
]));
$driver->insert('testDelete', new DSO([
'dso'=>['id'=>'item-b-2','type'=>'type-b','deleted'=>100],
'foo'=>'quz',
'sort'=>'d'
]));
}
protected function setup_testSelect()
{
$driver = $this->createDriver();
$driver->insert('testSelect', new DSO([
'dso'=>['id'=>'item-a-1','type'=>'type-a'],
'foo'=>'bar',
'sort'=>'a'
]));
$driver->insert('testSelect', new DSO([
'dso'=>['id'=>'item-a-2','type'=>'type-a'],
'foo'=>'baz',
'sort'=>'c'
]));
$driver->insert('testSelect', new DSO([
'dso'=>['id'=>'item-b-1','type'=>'type-b'],
'foo'=>'buz',
'sort'=>'b'
]));
$driver->insert('testSelect', new DSO([
'dso'=>['id'=>'item-b-2','type'=>'type-b','deleted'=>100],
'foo'=>'quz',
'sort'=>'d'
]));
}
/**
* 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 testCreateTable');
$pdo->exec('DROP TABLE testInsert');
$pdo->exec('DROP TABLE testSelect');
$pdo->exec('DROP TABLE testDelete');
}
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,18 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MySQL;
use PHPUnit\Framework\TestCase;
use Destructr\Drivers\AbstractDriverIntegrationTest;
use Destructr\Drivers\MySQLDriver;
class MySQLDriverIntegrationTest extends AbstractDriverIntegrationTest
{
const DRIVER_CLASS = \Destructr\Drivers\MySQLDriver::class;
const DRIVER_DSN = 'mysql:host=mysql;dbname=destructr_test';
const DRIVER_USERNAME = 'root';
const DRIVER_PASSWORD = 'badpassword';
const DRIVER_OPTIONS = null;
const TEST_TABLE = 'integrationtest';
}

View file

@ -1,17 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MySQL;
use PHPUnit\Framework\TestCase;
use Destructr\Drivers\AbstractDriverTest;
use Destructr\Drivers\MySQLDriver;
class MySQLDriverTest extends AbstractDriverTest
{
const DRIVER_CLASS = MySQLDriver::class;
const DRIVER_DSN = 'mysql:host=mysql;dbname=destructr_test';
const DRIVER_USERNAME = 'root';
const DRIVER_PASSWORD = 'badpassword';
const DRIVER_OPTIONS = null;
}

View file

@ -1,112 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/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,65 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr;
class HarnessDriver implements Drivers\DSODriverInterface
{
const EXTENSIBLE_VIRTUAL_COLUMNS = true;
public $last_select;
public $last_insert;
public $last_update;
public $last_delete;
public function __construct(string $dsn, string $username=null, string $password=null, array $options=null)
{
}
public function createTable(string $table, array $virtualColumns) : 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

@ -1,23 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\LegacyDrivers\SQLite;
use PHPUnit\Framework\TestCase;
use Destructr\Drivers\AbstractDriverIntegrationTest;
use Destructr\LegacyDrivers\SQLiteDriver;
class MySQLDriverTest extends AbstractDriverIntegrationTest
{
const DRIVER_CLASS = SQLiteDriver::class;
const DRIVER_DSN = 'sqlite:'.__DIR__.'/integration.test.sqlite';
const DRIVER_USERNAME = null;
const DRIVER_PASSWORD = null;
const DRIVER_OPTIONS = null;
const TEST_TABLE = 'sqliteintegrationtest';
public static function setUpBeforeClass()
{
@unlink(__DIR__.'/integration.test.sqlite');
}
}

View file

@ -1,22 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\LegacyDrivers\SQLite;
use PHPUnit\Framework\TestCase;
use Destructr\Drivers\AbstractDriverTest;
use Destructr\LegacyDrivers\SQLiteDriver;
class SQLiteDriverTest extends AbstractDriverTest
{
const DRIVER_CLASS = SQLiteDriver::class;
const DRIVER_DSN = 'sqlite:'.__DIR__.'/driver.test.sqlite';
const DRIVER_USERNAME = null;
const DRIVER_PASSWORD = null;
const DRIVER_OPTIONS = null;
public static function setUpBeforeClass()
{
@unlink(__DIR__.'/driver.test.sqlite');
}
}

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'));
}
}