CakePHP3: How to change an association strategy on-the-fly? - php

I would like to change an association strategy (hasMany) on the fly to "in" (default) to "select". Because this will correct the result for this situation:
"Get all publishers and only the first five books":
$publishersTable = TableRegistry::getTableLocator()->get('Publishers');
$publishersTable->getAssociation('Books')->setStrategy('select');
$query = $publishersTable->find()
->contain(['Books'=> function(Query $q){
return $q->limit(5);
}]);
Unfortunately, Cake still using "in" to run the query and not "separated queries" and the result is only 5 publishers (and not all publishers with the first 5 books).
Is it possible to change the strategy on-the-fly?
Thanks in advance !

A hasMany association will always use a single separate query, never multiple separate queries. The difference between the select and subquery strategies is that one will directly compare against an array of primary keys, and the other against a joined subquery that will match the selected parent records.
What you are trying is to select the greatest-n-per-group, that's not possible with the built in association loaders, and it can be a little tricky depending on the DBMS that you are using, check for example How to limit contained associations per record/group? for an example for MySQL < 8.x using a custom association and loader.
For DBMS that do support it, look into window functions. Here's an example of a loader that uses native window functions, it should be possible to simply replace the one in the linked example with it, but keep in mind that it's not really tested or anything, I just had it laying around from some experiments:
namespace App\ORM\Association\Loader;
use Cake\Database\Expression\OrderByExpression;
use Cake\ORM\Association\Loader\SelectLoader;
class GroupLimitedSelectLoader extends SelectLoader
{
/**
* The group limit.
*
* #var int
*/
protected $limit;
/**
* The target table.
*
* #var \Cake\ORM\Table
*/
protected $target;
/**
* {#inheritdoc}
*/
public function __construct(array $options)
{
parent::__construct($options);
$this->limit = $options['limit'];
$this->target = $options['target'];
}
/**
* {#inheritdoc}
*/
protected function _defaultOptions()
{
return parent::_defaultOptions() + [
'limit' => $this->limit,
];
}
/**
* {#inheritdoc}
*/
protected function _buildQuery($options)
{
$key = $this->_linkField($options);
$keys = (array)$key;
$filter = $options['keys'];
$finder = $this->finder;
if (!isset($options['fields'])) {
$options['fields'] = [];
}
/* #var \Cake\ORM\Query $query */
$query = $finder();
if (isset($options['finder'])) {
list($finderName, $opts) = $this->_extractFinder($options['finder']);
$query = $query->find($finderName, $opts);
}
$rowNumberParts = ['ROW_NUMBER() OVER (PARTITION BY'];
for ($i = 0; $i < count($keys); $i ++) {
$rowNumberParts[] = $query->identifier($keys[$i]);
if ($i < count($keys) - 1) {
$rowNumberParts[] = ',';
}
}
$rowNumberParts[] = new OrderByExpression($options['sort']);
$rowNumberParts[] = ')';
$rowNumberField = $query
->newExpr()
->add($rowNumberParts)
->setConjunction('');
$rowNumberSubQuery = $this->target
->query()
->select(['__row_number' => $rowNumberField])
->where($options['conditions']);
$columns = $this->target->getSchema()->columns();
$rowNumberSubQuery->select(array_combine($columns, $columns));
$rowNumberSubQuery = $this->_addFilteringCondition($rowNumberSubQuery, $key, $filter);
$fetchQuery = $query
->select($options['fields'])
->from([$this->targetAlias => $rowNumberSubQuery])
->where([$this->targetAlias . '.__row_number <=' => $options['limit']])
->eagerLoaded(true)
->enableHydration($options['query']->isHydrationEnabled());
if (!empty($options['contain'])) {
$fetchQuery->contain($options['contain']);
}
if (!empty($options['queryBuilder'])) {
$fetchQuery = $options['queryBuilder']($fetchQuery);
}
$this->_assertFieldsPresent($fetchQuery, $keys);
return $fetchQuery;
}
}

Thanks #ndm but I found another shorter solution:
$publishersTable->find()
->formatResults(function ($results) use ($publishersTable) {
return $results->map(function ($row) use ($publishersTable) {
$row['books'] = $publishersTable->Books->find()
->where(['publisher_id'=>$row['id']])
->limit(5)
->toArray();
return $row;
});
});

Related

PHP, Static Analysis, and Recursive Type Checking

I'm looking at a Database ORM that uses an array to define the WHERE clause, e.g.
$articles->find('all', [
'OR' => [
'category_id IS NULL',
'category_id' => $id,
],
]);
The "array keys" become part of the SQL, so they must be a developer defined string (aka a literal-string), otherwise you can have mistakes like this:
$articles->find('all', [
'OR' => [
'category_id IS NULL',
'category_id = ' . $id, // INSECURE, SQLi
],
]);
If the "array values" simply contained the values to be parameterised (i.e. user values), then I could specify the parameter type as array<literal-string, int|string>.
But, as you will notice with the 'OR' key, the parameter can contain nested arrays, and can be many levels deep.
Is it possible to get Static Analysis tools like Psalm or PHPStan to work with this?
I can use the CakePHP implementation as an example of how this works:
<?php
class orm {
/**
* #param array<int, literal-string|array<mixed>>|array<literal-string, int|string|array<mixed>> $conditions
*/
public function find(string $finder, array $conditions): void {
print_r($this->_addConditions($conditions));
}
/**
* #param array<int, literal-string|array<mixed>>|array<literal-string, int|string|array<mixed>> $conditions
* #param literal-string $conjunction
* #return array{literal-string, array<int, mixed>}
*/
private function _addConditions(array $conditions, string $conjunction = 'AND'): array {
// https://github.com/cakephp/cakephp/blob/ab052da10dc5ceb2444c29aef838d10844fe5995/src/Database/Expression/QueryExpression.php#L654
$operators = ['and', 'or', 'xor'];
$sql = [];
$parameters = [];
foreach ($conditions as $k => $c) {
if (is_numeric($k)) {
if (is_array($c)) {
/** #var array<int, array<mixed>> $sub_conditions */
$sub_conditions = $c;
list($new_sql, $new_parameters) = $this->_addConditions($sub_conditions, 'AND');
$sql[] = $new_sql;
$parameters = array_merge($parameters, $new_parameters);
} else if (is_string($c)) {
$sql[] = $c; // $c must be a literal-string
}
} else {
$operatorId = array_search(strtolower($k), $operators);
if ($operatorId !== false) {
/** #var array<literal-string, int|string|array<mixed>> $sub_conditions */
$sub_conditions = $c;
list($new_sql, $new_parameters) = $this->_addConditions($sub_conditions, $operators[$operatorId]);
$sql[] = $new_sql;
$parameters = array_merge($parameters, $new_parameters);
} else {
$sql[] = $k . ' = ?'; // $k must be a literal-string
$parameters[] = $c;
}
}
}
/** #var literal-string $sql */
$sql = '(' . implode(' ' . $conjunction . ' ', $sql) . ')';
return [$sql, $parameters];
}
}
$articles = new orm();
?>
PHPStan
"Recursive types aren't supported now and I don't know if they ever will" Mar 2021.
"PHPStan doesn't support recursive types as they're very hard to do" May 2022.
Psalm
"Recursive types aren’t supported by Psalm (and probably won’t ever be tbh)" Jul 2019.
"Yeah, you can't - if you search issues you'll see a few requests for recursive types that have come up (and I've dismissed)" Feb 2020.

Using transactions in CakePHP TreeBehavior?

We have been using TreeBehavior with CakePHP 3.6 for a while now. Lately, with a big tree (some 90000 nodes) we are running into problems.
When deleting a node, the beforeDelete() function is called in cakephp/src/ORM/Behavior/TreeBehavior:
/**
* Also deletes the nodes in the subtree of the entity to be delete
*
* #param \Cake\Event\Event $event The beforeDelete event that was fired
* #param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
* #return void
*/
public function beforeDelete(Event $event, EntityInterface $entity)
{
$config = $this->getConfig();
$this->_ensureFields($entity);
$left = $entity->get($config['left']);
$right = $entity->get($config['right']);
$diff = $right - $left + 1;
if ($diff > 2) {
$query = $this->_scope($this->_table->query())
->delete()
->where(function ($exp) use ($config, $left, $right) {
/* #var \Cake\Database\Expression\QueryExpression $exp */
return $exp
->gte($config['leftField'], $left + 1)
->lte($config['leftField'], $right - 1);
});
$statement = $query->execute();
$statement->closeCursor();
}
$this->_sync($diff, '-', "> {$right}");
}
This function calls the _sync() function to update the left and right values of the tree:
/**
* Auxiliary function used to automatically alter the value of both the left and
* right columns by a certain amount that match the passed conditions
*
* #param int $shift the value to use for operating the left and right columns
* #param string $dir The operator to use for shifting the value (+/-)
* #param string $conditions a SQL snipped to be used for comparing left or right
* against it.
* #param bool $mark whether to mark the updated values so that they can not be
* modified by future calls to this function.
* #return void
*/
protected function _sync($shift, $dir, $conditions, $mark = false)
{
$config = $this->_config;
foreach ([$config['leftField'], $config['rightField']] as $field) {
$query = $this->_scope($this->_table->query());
$exp = $query->newExpr();
$movement = clone $exp;
$movement->add($field)->add((string)$shift)->setConjunction($dir);
$inverse = clone $exp;
$movement = $mark ?
$inverse->add($movement)->setConjunction('*')->add('-1') :
$movement;
$where = clone $exp;
$where->add($field)->add($conditions)->setConjunction('');
$query->update()
->set($exp->eq($field, $movement))
->where($where);
$query->execute()->closeCursor();
}
}
However, it seems that the update queries performed by this _sync() function are not wrapped in a transaction, and look like this:
UPDATE `leafs` SET `lft` = ((`lft` - 12)) WHERE ((`lft` > 52044))
UPDATE `leafs` SET `rght` = ((`rght` - 12)) WHERE ((`rght` > 52044))
Should these not be wrapped in a transaction, like this?
START TRANSACTION
UPDATE `leafs` SET `lft` = ((`lft` - 12)) WHERE ((`lft` > 52044))
UPDATE `leafs` SET `rght` = ((`rght` - 12)) WHERE ((`rght` > 52044))
COMMIT
... especially since the values that are updated (lft and rght) also occur in the WHERE clause. We are having some issues with the tree becoming corrupted and are wondering whether this might be a cause... especially when multiple operations are performed in quick succession on a big tree.

Laravel use dynamic mutators in where clause

So i have two columns - SizeX & SizeY. For front end users I use Laravel mutator
public function getSizeAttribute(){
/**
* Set Size
*/
$size = $this->SizeX. " x ". $this->SizeY;
/**
* Return
*/
return $size;
}
To format the sizes like this SizeX x SizeY. The column Sizes does not exists because its dynamic. Is it possible to use mutators or alternative within the Elaquent model to detect that this is a dynamic attribute, and use some sort of method to convert the SizeX x SizeY to individual columns? Once the user submits the attribute Size back to laravel application for filtering?
Edit:
Right, this is the method I'm using to retrieve filtered Items
public function scopeFilteredMaterials($query,$params = array()){
/**
* Get filters
*/
$filters = array();
/**
* Extract Info
*/
if(!empty($params) && is_array($params)){
/**
* Get Available Filters
*/
$filters = $this->getAvailableFilters();
foreach ($filters as $key => $filter){
if(isset($params[$filter])){
$filters[$filter] = $params[$filter];
unset($filters[$key]);
}else{
unset($filters[$key]);
}
}
}
foreach ($filters as $key => $filter){
$query->whereIn(key ,$filter);
}
$result = $query->get();
}
This is the filters variable which holds available filters for user to see
protected $filters = array(
"Name",
"url",
"Size",
);
I'm using the above to show the specific values to the user. Once the user selects those values I'm using the same array to check against those filters and fire the query. My problem is the Size attribute is made up of two columns which I have not problem using the following Mutator and $appends variable to automatically bring the value to the user.
/**
* Get Size Attribute
*/
public function getSizeAttribute(){
/**
* Set Size
*/
$size = $this->SizeX. " x ". $this->SizeY;
/**
* Return
*/
return $size;
}
But i ca't figure out a way to convert the Size variable back to SizeX & SizeY
If you are always creating the composite variable through the accessor, then you can use a scope that parses this string. Something like this:
public function scopeSize($query, $size)
{
$sizes = explode(' x ', $size);
return $query->where('SizeX', $sizes[0])->where('SizeY', $sizes[1]);
}
You can then use this scope in the filter method or anywhere else.
If you want to create a new dynamic attribute, just create an accessor:
public function getSizesAttribute()
{
return $this->SizeX . ' x ' . $this->SizeY;
}
If you want to set SizeX and SizeY automatically in case if Sizes property exists. In this case, you can create mutators for both SizeX and SizeY. For example:
public function setSizeXAttribute($value)
{
if (!empty($this->attributes['Sizes'])) {
$this->attributes['SizeX'] = explode(' x ', $this->attributes['Sizes']))[0];
}
}
public function setSizeYAttribute($value)
{
if (!empty($this->attributes['Sizes'])) {
$this->attributes['SizeY'] = explode(' x ', $this->attributes['Sizes']))[1];
}
}
PS: This is a solution for standard snake_case properties, like size_x. I'd really recommend you to use these in Laravel. If you still want to use SizeX, you should also do this.

How to generate unique random value for each user in laravel and add it to database

I am developing a event organization website. Here when the user registers for an event he will be given a unique random number(10 digit), which we use to generate a barcode and mail it to him. Now,
I want to make the number unique for each registered event.
And also random
One solution is to grab all the random numbers in an array and generate a random number using Php rand(1000000000, 9999999999) and loop through and check all the values. Grab the first value that doesn't equal to any of the values in the array and add it to the database.
But I am thinking that there might be a better solution to this. Any suggestion?
You can use php's uniqid() function to generate a unique ID based on the microtime (current time in microseconds)
Example:
<?php
echo uniqid();
?>
Output:
56c3096338cdb
Your logic isn't technically faulty. However, if your application attracts lots of users, fetching all of the random numbers may well become unnecessarily expensive, in terms of resources and computation time.
I would suggest another approach, where you generate a random number and then check it against the database.
function generateBarcodeNumber() {
$number = mt_rand(1000000000, 9999999999); // better than rand()
// call the same function if the barcode exists already
if (barcodeNumberExists($number)) {
return generateBarcodeNumber();
}
// otherwise, it's valid and can be used
return $number;
}
function barcodeNumberExists($number) {
// query the database and return a boolean
// for instance, it might look like this in Laravel
return User::whereBarcodeNumber($number)->exists();
}
This is good:
do {
$refrence_id = mt_rand( 1000000000, 9999999999 );
} while ( DB::table( 'transations' )->where( 'RefrenceID', $refrence_id )->exists() );
To avoid the problem of having to check to see if a matching code exists every time a new one is created, I just catch MySQL's duplicate record exception (error code 1062). If that error is caught, I just call the function again until the save is successful. That way, it only has to generate a new code if it collides with an existing one. Runs a lot faster -- but obviously gets a bit slower as your number of users approaches the number of possible barcodes.
function generateBarcode($user_id) {
try {
$user = User::find($user_id);
$user->barcode = mt_rand(1000000000, 9999999999);
$user->save();
} catch (Exception $e) {
$error_info = $e->errorInfo;
if($error_info[1] == 1062) {
generateBarcode($user_id);
} else {
// Only logs when an error other than duplicate happens
Log::error($e);
}
}
}
So just loop through all the users you want to assign a code to:
foreach(User::all() as $user) {
generateBarcode($user->id);
}
You could also add some logic to escape the function loop if a maximum number of attempts are made, but I've never bothered because collisions are unlikely.
Looping through the array won't be that efficient. If your database becomes too large then it slow down the entire process and also there might be a rare situation when 2 threads are looping through the array for the same random number and it will be found available and return same number to both the tickets.
So instead of looping through the array you can set the 10 digit registration id as primary key and instead of looping through the array you can insert the registration details along with randomly generated number, if the database insert operation is successful you can return the registration id but if not then regenerate the random number and insert.
Alternate solution which will be more effective
Instead of 10 digit random numbers you can use timestamp to generate a 10 digit unique registration number and to make it random you can randomize the first 2 or 3 digits of the timestamp
One Solution could be like this:
use Illuminate\Support\Facades\Validator;
private function genUserCode(){
$this->user_code = [
'user_code' => mt_rand(1000000000,9999999999)
];
$rules = ['user_code' => 'unique:users'];
$validate = Validator::make($this->user_code, $rules)->passes();
return $validate ? $this->user_code['user_code'] : $this->genUserCode();
}
Its generating a random number between 1000000000 and 9999999999. After that, it validates the number against the table. If true then it returns the number, otherwise runs the function again.
I made something like this
/**
* Generate unique shipment ID
*
* #param int $length
*
* #return string
*/
function generateShipmentId($length)
{
$number = '';
do {
for ($i=$length; $i--; $i>0) {
$number .= mt_rand(0,9);
}
} while ( !empty(DB::table('shipments')->where('id', $number)->first(['id'])) );
return $number;
}
<?php
declare(strict_types=1);
namespace App\Helpers;
use App\Exceptions\GeneratorException;
class GeneratorHelper
{
public static $limitIterations = 100000;
/**
* #param string $column
* #param string $modelClass
* #return string
* #throws GeneratorException
*/
public static function generateID(string $modelClass, string $column): string
{
return self::run(
$modelClass,
$column,
self::IDGenerator(),
'Generation id is failed. The loop limit exceeds ' . self::$limitIterations
);
}
/**
* #param string $modelClass
* #param string $column
* #param \Generator $generator
* #param string $exceptionMessage
* #param array $whereParams
* #return string
* #throws GeneratorException
*/
protected static function run(string $modelClass, string $column, \Generator $generator, string $exceptionMessage, array $whereParams = []): string
{
try {
foreach ($generator as $id) {
$query = $modelClass::where([$column => $id]);
foreach ($whereParams as $param) {
$query->where(...$param);
}
if (!$query->first()) {
return $id;
}
}
} catch (\Throwable $e) {
$exceptionMessage = $e->getMessage();
}
throw new GeneratorException($exceptionMessage);
}
protected static function IDGenerator(): ?\Generator
{
for ($i = 1; $i <= self::$limitIterations; $i++) {
yield (string)random_int(1000000000, 9999999999);
}
return null;
}
}
sample usage
$card->number = GeneratorHelper::generateID(Card::class, 'number');
for me, I prefer using MySQL way, because when you have a large amount of data in your DB, you will have too much quires to check your number uniqueness,
for example, a code like this:
do {
$code = random_int(100000, 99999999);
}
while (AgentOrder::where("order_number", "=", $code)->exists());
so , this "do while" loop would be excueted too many times.
to avoid that, you can use MySQL way like:
private function getUniqueCodeNumber()
{
$value = AgentOrder::query()->selectRaw('FLOOR(1000000 + RAND() * 10000000) AS generatedCode')
->whereRaw("'generatedCode' NOT IN (SELECT order_number FROM agent_orders WHERE agent_orders.order_number IS NOT NULL)")
->limit(1)->first();
if ($value == null) return 100000000;
$value = (int)$value->generatedCode;
return $value;
}
this answer is inspired from this answer.
Helper (app/Helpers/Constants.php)
<?php
namespace App\Helper;
class Constants
{
public static function getUniqueId($model){
$id = mt_rand(1000000000, 9999999999);
if($model->find($id))
return self::getUniqueId($model);
return $id;
}
}
?>
Model (app/Models/User.php)
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public static function boot(){
parent::boot();
$creationCallback = function ($model) {
if (empty($model->{$model->getKeyName()}))
$model->{$model->getKeyName()} = Constants::getUniqueId(new self());
};
static::creating($creationCallback);
}
}
?>
Explanation: Instead of calling the getUniqueId method in every controller. You can write it inside model.
From the #Joel Hinz answer :
public function set_number() {
$number = mt_rand(1000000000, 9999999999);
return User::where('number', $number)->exists() ? $this->set_number() : $number;
}
Hope that helped.

How to represent graph datastructure using PHP array

Recently, I was given a small task of using graph data structure as core to make a web application. I started out with an idea of simple path optimization problem, which can be completed in few days. The problem is that I am not able decide the correct framework for this task. Using just PHP was the only thing i could think of given the time constraint.
So, how can I represent a graph data structure using PHP's custom data structure( array).
Furthermore,can you suggest some other frameworks on which i can work for this task.
You can use the array to keep an adjacency list.
PHP's array has two uses: it can be a list of objects, or an associative array, which associates one object with another. You can also use an associative array as a poor man's set, by keeping the using the data from the set as keys in the associative array. Since it is usual to associate data to vertices, and edges, we can actually make use of this in a natural way. Following is an example of an undirected graph class.
<?php
/**
* Undirected graph implementation.
*/
class Graph
{
/**
* Adds an undirected edge between $u and $v in the graph.
*
* $u,$v can be anything.
*
* Edge (u,v) and (v,u) are the same.
*
* $data is the data to be associated with this edge.
* If the edge (u,v) already exists, nothing will happen (the
* new data will not be assigned).
*/
public function add_edge($u,$v,$data=null)
{
assert($this->sanity_check());
assert($u != $v);
if ($this->has_edge($u,$v))
return;
//If u or v don't exist, create them.
if (!$this->has_vertex($u))
$this->add_vertex($u);
if (!$this->has_vertex($v))
$this->add_vertex($v);
//Some sanity.
assert(array_key_exists($u,$this->adjacency_list));
assert(array_key_exists($v,$this->adjacency_list));
//Associate (u,v) with data.
$this->adjacency_list[$u][$v] = $data;
//Associate (v,u) with data.
$this->adjacency_list[$v][$u] = $data;
//We just added two edges
$this->edge_count += 2;
assert($this->has_edge($u,$v));
assert($this->sanity_check());
}
public function has_edge($u,$v)
{
assert($this->sanity_check());
//If u or v do not exist, they surely do not make up an edge.
if (!$this->has_vertex($u))
return false;
if (!$this->has_vertex($v))
return false;
//some extra sanity.
assert(array_key_exists($u,$this->adjacency_list));
assert(array_key_exists($v,$this->adjacency_list));
//This is the return value; if v is a neighbor of u, then its true.
$result = array_key_exists($v,$this->adjacency_list[$u]);
//Make sure that iff v is a neighbor of u, then u is a neighbor of v
assert($result == array_key_exists($u,$this->adjacency_list[$v]));
return $result;
}
/**
* Remove (u,v) and return data.
*/
public function remove_edge($u,$v)
{
assert($this->sanity_check());
if (!$this->has_edge($u,$v))
return null;
assert(array_key_exists($u,$this->adjacency_list));
assert(array_key_exists($v,$this->adjacency_list));
assert(array_key_exists($v,$this->adjacency_list[$u]));
assert(array_key_exists($u,$this->adjacency_list[$v]));
//remember data.
$data = $this->adjacency_list[$u][$v];
unset($this->adjacency_list[$u][$v]);
unset($this->adjacency_list[$v][$u]);
//We just removed two edges.
$this->edge_count -= 2;
assert($this->sanity_check());
return $data;
}
//Return data associated with (u,v)
public function get_edge_data($u,$v)
{
assert($this->sanity_check());
//If no such edge, no data.
if (!$this->has_edge($u,$v))
return null;
//some sanity.
assert(array_key_exists($u,$this->adjacency_list));
assert(array_key_exists($v,$this->adjacency_list[$u]));
return $this->adjacency_list[$u][$v];
}
/**
* Add a vertex. Vertex must not exist, assertion failure otherwise.
*/
public function add_vertex($u,$data=null)
{
assert(!$this->has_vertex($u));
//Associate data.
$this->vertex_data[$u] = $data;
//Create empty neighbor array.
$this->adjacency_list[$u] = array();
assert($this->has_vertex($u));
assert($this->sanity_check());
}
public function has_vertex($u)
{
assert($this->sanity_check());
assert(array_key_exists($u,$this->vertex_data) == array_key_exists($u,$this->adjacency_list));
return array_key_exists($u,$this->vertex_data);
}
//Returns data associated with vertex, null if vertex does not exist.
public function get_vertex_data($u)
{
assert($this->sanity_check());
if (!array_key_exists($u,$this->vertex_data))
return null;
return $this->vertex_data[$u];
}
//Count the neighbors of a vertex.
public function count_vertex_edges($u)
{
assert($this->sanity_check());
if (!$this->has_vertex($u))
return 0;
//some sanity.
assert (array_key_exists($u,$this->adjacency_list));
return count($this->adjacency_list[$u]);
}
/**
* Return an array of neighbor vertices of u.
* If $with_data == true, then it will return an associative array, like so:
* {neighbor => data}.
*/
public function get_edge_vertices($u,$with_data=false)
{
assert($this->sanity_check());
if (!array_key_exists($u,$this->adjacency_list))
return array();
$result = array();
if ($with_data) {
foreach( $this->adjacency_list[$u] as $v=>$data)
{
$result[$v] = $data;
}
} else {
foreach( $this->adjacency_list[$u] as $v=>$data)
{
array_push($result, $v);
}
}
return $result;
}
//Removes a vertex if it exists, and returns its data, null otherwise.
public function remove_vertex($u)
{
assert($this->sanity_check());
//If the vertex does not exist,
if (!$this->has_vertex($u)){
//Sanity.
assert(!array_key_exists($u,$this->vertex_data));
assert(!array_key_exists($u,$this->adjacency_list));
return null;
}
//We need to remove all edges that this vertex belongs to.
foreach ($this->get_edge_vertices($u) as $v)
{
$this->remove_edge($u,$v);
}
//After removing all such edges, u should have no neighbors.
assert($this->count_vertex_edges($u) == 0);
//sanity.
assert(array_key_exists($u,$this->vertex_data));
assert(array_key_exists($u,$this->adjacency_list));
//remember the data.
$data = $this->vertex_data[$u];
//remove the vertex from the data array.
unset($this->vertex_data[$u]);
//remove the vertex from the adjacency list.
unset($this->adjacency_list[$u]);
assert($this->sanity_check());
return $data;
}
public function get_vertex_count()
{
assert($this->sanity_check());
return count($this->vertex_data);
}
public function get_edge_count()
{
assert($this->sanity_check());
//edge_count counts both (u,v) and (v,u)
return $this->edge_count/2;
}
public function get_vertex_list($with_data=false)
{
$result = array();
if ($with_data)
foreach ($this->vertex_data as $u=>$data)
$result[$u]=$data;
else
foreach ($this->vertex_data as $u=>$data)
array_push($result,$u);
return $result;
}
public function edge_list_str_array($ordered=true)
{
$result_strings = array();
foreach($this->vertex_data as $u=>$udata)
{
foreach($this->adjacency_list[$u] as $v=>$uv_data)
{
if (!$ordered || ($u < $v))
array_push($result_strings, '('.$u.','.$v.')');
}
}
return $result_strings;
}
public function sanity_check()
{
if (count($this->vertex_data) != count($this->adjacency_list))
return false;
$edge_count = 0;
foreach ($this->vertex_data as $v=>$data)
{
if (!array_key_exists($v,$this->adjacency_list))
return false;
$edge_count += count($this->adjacency_list[$v]);
}
if ($edge_count != $this->edge_count)
return false;
if (($this->edge_count % 2) != 0)
return false;
return true;
}
/**
* This keeps an array that associates vertices with their neighbors like so:
*
* {<vertex> => {<neighbor> => <edge data>}}
*
* Thus, each $adjacency_list[$u] = array( $v1 => $u_v1_edge_data, $v2 => $u_v2_edge_data ...)
*
* The edge data can be null.
*/
private $adjacency_list = array();
/**
* This associates each vertex with its data.
*
* {<vertex> => <data>}
*
* Thus each $vertex_data[$u] = $u_data
*/
private $vertex_data = array();
/**
* This keeps tracks of the edge count so we can retrieve the count in constant time,
* instead of recounting. In truth this counts both (u,v) and (v,u), so the actual count
* is $edge_count/2.
*/
private $edge_count = 0;
}
$G = new Graph();
for ($i=0; $i<5; ++$i)
{
$G->add_vertex($i);
}
for ($i=5; $i<10; ++$i)
{
$G->add_edge($i,$i-5);
}
print 'V: {'.join(', ',$G->get_vertex_list())."}\n";
print 'E: {'.join(', ',$G->edge_list_str_array())."}\n";
$G->remove_vertex(1);
print 'V: {'.join(', ',$G->get_vertex_list())."}\n";
print 'E: {'.join(', ',$G->edge_list_str_array())."}\n";
$G->remove_vertex(1);
print 'V: {'.join(', ',$G->get_vertex_list())."}\n";
print 'E: {'.join(', ',$G->edge_list_str_array())."}\n";
?>

Categories