GOAL:
I want to build a "market validation control engine" that validates players' actions and limits on the market.
SUMMARY:
Consider that we have a Character object that can buy only a given number of Item objects, e.g Arrow. The number depends on Race, Tire and Area where a lower layout of hierarchy overrides the upper; if the limit isn't specified lower layout inherits it from the upper:
For example:
Pro ELF in Forest can buy up to 2000 arrows
Basic Ork in Forest and Basic Ork in Desert can buy 50 arrows.
Additionally, the Character object can Buy and Sell a specific amount of arrows on the market. The amount and action depend on the Area. Otherway to say is every node might have its validation rules.
For example:
Basic Elf in Desert can sell 50% arrows
Basic Orc in Desert can sell 100% of the arrows.
Lastly, based on the Tier players can Exchange arrows to another item. For example :
Pro ELF can exchange 100% of its arrows
STRUGGLES:
Building hierarchy
Validation rules. Every layout of hierarchy (Tier or Area) have rules
How to make 1 and 3 work together
Scalability. Adding a new layout (e.g. Clan) or attaching a new validation rule to Area
Maintenance
I am almost sure that I need multiple different patterns here. Currently, I'm thinking of the Decorator Pattern for creating hierarchy object with all the limits and the Chain of Responsibility for the rule validation. However, I am not sure how well these patterns will work together in the long ran.
UPD: In the example, I used Arrow - one of the hundreds of items on the market and Desert with Forest - two of the 9 areas.
There's quite a few different ways to implement this so I'm assuming you're not really sure where to start.
I've written up some example code using classes. In this, there's 2 ways that we could store the item (arrow) data.
Nested Array - This allows for a more "structured" approach. It also allows you to easily add more attributes to each "child" array if necessary.
Serialized Map - This has a much smaller code footprint and is generally quicker.
NOTE: This code is not setup to be well-performing (in reference to speed) but as a way to see how you could setup this system.
There are 3 files here index.php, Character.class.php, Market.class.php.
There are 2 main methods on the Market class: getNestedLimit() and getSerializedLimit(). Each one shows how you would get the limit based on those example structures.
<?php
// index.php
require_once __DIR__ . "/Character.class.php";
require_once __DIR__ . "/Market.class.php";
$market = new Market;
$orc_a = new Character("orc", "basic", "desert");
$orc_b = new Character("orc", "pro", "forest");
$market->getNestedLimit($orc_a, "arrow"); // 50
$market->getNestedLimit($orc_b, "arrow"); // 150
$elf_a = new Character("elf", "pro", "forest");
$elf_b = new Character("elf", "pro", "desert");
$market->getSerializedLimit($elf_a, "arrow"); // 2000
$market->getSerializedLimit($elf_b, "arrow"); // 50
<?php
// Character.class.php
class Character {
public string $race;
public ?string $tier = null;
public ?string $area = null;
public function __construct(
string $race,
?string $tier = null,
?string $area = null,
) {
$this->race = $race;
$this->tier = $tier;
$this->area = $area;
}
}
// Market.class.php
class Market {
public $nested_items = [
"arrow" => [
"limit" => [
"_base" => 0,
"race" => [
"orc" => [
"_base" => 50,
"tier" => [
"basic" => [
"area" => [],
],
"pro" => [
"area" => [
"desert" => 50,
"forest" => 150,
],
],
],
],
"elf" => [
"_base" => 1000,
"tier" => [
"basic" => [
"area" => [
"desert" => 150,
"forest" => 1000,
],
],
"pro" => [
"area" => [
"desert" => 50,
"forest" => 2000,
],
],
],
],
],
],
],
];
public function getNestedLimit(Character $character, string $item_id) {
$item = $this->nested_items[$item_id];
$item_limit = $item["limit"];
$actual_limit = $item_limit["_base"];
// check race
if (isset($item_limit["race"][$character->race])) {
$race_limit = $item_limit["race"][$character->race];
if (isset($race_limit["_base"])) {
// value exists - use this
$actual_limit = $race_limit["_base"];
}
// check tier
if (isset($race_limit["tier"][$character->tier])) {
$tier_limit = $race_limit["tier"][$character->tier];
if (isset($tier_limit["_base"])) {
// value exists - use this
$actual_limit = $tier_limit["_base"];
}
// check area
if (isset($tier_limit["area"][$character->area])) {
$actual_limit = $tier_limit["area"][$character->area];
}
}
}
return $actual_limit;
}
/** Serialize syntax:
* {race}[-{tier}[-{area}]]
* e.g.
* "Pro tier orc in forest" -> "o-p-f"
* "Basic tier elf" -> "e-b"
* "Orc" -> "o"
*
* When determining the limit, pick the most specific first.
*/
public $serialized_items = [
"arrow" => [
"limit" => [
"_base" => 0,
// orc
"o" => 50,
"o-p-d" => 50,
"o-p-f" => 150,
// elf
"e" => 1000,
"e-b-d" => 150,
"e-b-f" => 1000,
"e-p-d" => 50,
"e-p-f" => 2000,
]
]
];
/** This "map" is purely for example - you may end up using ids
* or something more consistent.
* I would recommend using a different system, such as enums or constants.
*/
public $serial_keys = [
"orc" => "o",
"elf" => "e",
"basic" => "b",
"pro" => "p",
"desert" => "d",
"forest" => "f",
];
public function getSerializedLimit(Character $character, string $item_id) {
$item = $this->serialized_items[$item_id];
$item_limit = $item["limit"];
$actual_limit = $item_limit["_base"];
// serialize character
// this could be done in various ways - this is purely for example
// start with most specific
$serial = $this->serial_keys[$character->race] . "-" . $this->serial_keys[$character->tier] . "-" . $this->serial_keys[$character->area];
while ($serial !== "") {
if (isset($item_limit[$serial])) {
$actual_limit = $item_limit[$serial];
break;
}
// remove most specific section
// here we are expected each section to be a single character
// be removing 2 characters, we remove the hyphen as well
// NOTE: this hyphen is only here to help visualize
// you could very easily use 1 character without
// the hyphen and change this to `-1`
$serial = substr($serial, -2);
}
return $actual_limit;
}
}
EDIT: Adding example compiling nested array
As mentioned in the comments, you won't have to manually adjust the array - instead you could have it compile on load/initialise of the market. The code below shows an example datasource setup and code to compile the nested array.
This can also easily be adjusted to re-created the serialized array.
<?php
Datasource: `item`
| item_id | name | limit |
|---------|-------|-------|
| arrow | Arrow | 0 |
Datasource: race
| race_id | `name` |
|---------|--------|
| elf | Elf |
| orc | Orc |
Datasource: `tier`
| tier_id | name |
|---------|-------|
| basic | Basic |
| pro | Pro |
Datasource: `area`
| area_id | name |
|---------|--------|
| forest | Forest |
| desert | Desert |
Datasource: `item_limit`
| item_id | race_id | tier_id | area_id | limit |
|---------|---------|---------|---------|-------|
| arrow | orc | NULL | NULL | 50 |
| arrow | orc | pro | forest | 150 |
| arrow | elf | NULL | NULL | 1000 |
| arrow | elf | basic | desert | 150 |
| arrow | elf | pro | desert | 50 |
| arrow | elf | pro | forest | 2000 |
// Using these - compile to nested array (i.e. you only need to maintain the datasources, not the array)
$items = ... // results from `item` datasource;
$races = ... // results from `race` datasource;
$tiers = ... // results from `tier` datasource;
$areas = ... // results from `area` datasource;
$nested_array = [];
foreach($items as $item){
$limit_data = ["_base" => $item["limit"]];
// populate races
$limit_data["race"] = [];
foreach($races as $race){
$race_data = ["tier" => []];
// populate tiers
foreach($tiers as $tier){
$tier_data = ["area" => []];
foreach($areas as $area){
$tier_data["area"][$area["area_id"]] = [];
}
$race_data["tier"][$tier["tier_id"]] = $tier_data;
}
$limit_data["race"][$race["race_id"]] = $race_data;
}
$item_limits = ... // results from `item_limit` datasource filtered by `item_id`
foreach($item_limits as $item_limit){
// expects "less specific" columns to always be set
// i.e. if `area` is set, `tier` and `race` should be too
// assigning variable by reference to keep this example short
$parent =& $limit_data["race"][$item_limit["race_id"]];
if(!isset($item_limit["tier_id"])){
$parent["_base"] = $item_limit["limit"];
continue;
}
$parent =& $parent["tier"][$item_limit["tier_id"]];
if(!isset($item_limit["area_id"])){
$parent["_base"] = $item_limit["limit"];
continue;
}
$parent["area"][$item_limit["area_id"]] = $item_limit["limit"];
}
$nested_array[$item["item_id"]] = ["limit" => $limit_data];
}
Memory Considerations
The above code shows how you could implement a system like this. However, in a live environment with many items, it would be better to only initialise/compile the nested/serialized array for a specific item as needed. Caching is your friend here.
Alternatively you can take this same approach and implement it in such a way that it only needs to query the datasource to reduce the memory footprint.
There are two questions actually.
How to build the character hierarchy with design pattern
How to rule the market with validation
This is my modeling suggestion
To the first problems. There are different properties to define a Character. Race and Tier. I am not sure if Area is the location of the character position. Or it is the biome of the character. If latter, then the character properties become three. I suggest to model the hierarchy with composition over inheritance. Otherwise, using traditional inheritance would result in many combination of classes (default Orc, Basic Orc, Pro Orc etc). Not to mention if more Race or Tier to come in the future.
To the second problem, I would not recommend chain of responsibility. Because with CoR, you need to define a long chain. Each processing object handle a variation of Character. You will need many processing object similar to the first problem.
I suggest to have a data structure registered what kind of Character could trade how many items. In my suggestion this is the responsibility of MarketAuthority. Imagine it has a map-like structure inside, and each entry it has a record call MarketRegulation. MarketRegulation define what kind of Character can buy or sell how many items. When trade happens, pass the character to MarketAuthority and find which MarketRegulation match the character. From the MarketRegulation result you know how many the character can trade.
Below is the example of MarketAuthority implementation.
class MarketAuthority
{
private $regulations;
function __construct()
{
$this->regulations = array();
// setting up regulations. Ideally they should be come from data source
// orc
$orc = new Character(null, null, new Orc());
array_push($this->regulations, new MarketRegulation($orc, new ActualLimit(50), TradeType::BUY));
.
.
.
// basic orc
$basicOrc = new Character(new Basic(), null, new Orc());
array_push($this->regulations, new MarketRegulation($basicOrc, new ActualLimit(50), TradeType::BUY));
$proForestOrc = new Character(new Pro(), new Forest(), new Orc());
array_push($this->regulations, new MarketRegulation($proForestOrc, new ActualLimit(150), TradeType::BUY));
// more to be set ...
}
public function buyCheck(Character $character, int $quantity): bool
{
$regulation = $this->getRegulation($character, TradeType::BUY);
return $regulation->getLimit()->allow($character, $quantity);
}
public function sellCheck(Character $character, int $quantity): bool
{
$regulation = $this->getRegulation($character, TradeType::SELL);
return $regulation->getLimit()->allow($character, $quantity);
}
public function buy(Character $character, int $quantity)
{
if ($this->buyCheck($character, $quantity)) {
$arrow = $character->getArrow() + $quantity;
$character->setArrow($arrow);
}
}
public function sell(Character $character, int $quantity)
{
if ($this->sellCheck($character, $quantity)) {
$arrow = $character->getArrow();
$character->setArrow($arrow - $quantity);
}
}
public function getRegulation(Character $character, TradeType $tradeType): MarketRegulation
{
$regulation = array_filter($this->regulations, function (MarketRegulation $r) use ($character, $tradeType) {
return $r->getCharacterConfig()->equals($character) && $r->getTradeType() == $tradeType;
});
return array_pop($regulation);
}
}
For a runnable prototype, please visit https://github.com/stackoverflow-helpdesk/free-market. Below is the prototype console output, simulating how the MarketAuthority regulate trades.
Basic Desert Elf [0] can buy up to 150 arrow
Basic Desert Elf [0] buy arrow*150
Basic Desert Elf [150]
Basic Desert Elf [150] only sell up to 50%. i.e.75
Basic Desert Elf [150] sell arrow*80
Basic Desert Elf [150]
Basic Desert Elf [150] cannot sell arrow*80
Orc [0] can buy up to 50 arrow
Orc [0] buy arrow*80
Orc [0]
Orc [0] cannot buy arrow*80
Consider the following scenario for API testing
Given I am using the API Service
When I send the <request> as <method> to <endpoint> endpoint with <key> having value <value>
Then The response status code is 200
Examples:
| request | method | endpoint | key | value |
| "All Keys" | "POST" | "endpointName" | "numericField" | 15 |
| "All Keys" | "POST" | "endpointName" | "numericField" | 15.12345 |
The above example creates a request with the specified parameters.
My problem is that while the integer (15) value is passed to the function accordingly, the float (15.12345) is converted into a string ("15.12345"). This happens straight as the function is called; it is not modified later on during another step.
Is there a way to keep the float value from turning into a string?
As requested, the send request step method is:
$data = $this->fulfilmentOptions->getDataValue($request);
$uri = $this->getMinkParameter('base_url') . $this->setEndpoint($endpoint);
array_walk_recursive($data, function(&$item, $originalKey) use ($key, $value) {
if ($originalKey === $key) {
$item = $value;
}
});
try {
$this->response = $this->client->request($method, $uri, [
'headers' => $this->fulfilmentOptions->getDataValue('CreateOrder API Headers'),
'body' => json_encode($data)
]);
} catch (\GuzzleHttp\Exception\RequestException $e) {
$this->response = $e->getResponse();
}
$this->responseContent = $this->response->getBody()->getContents();
One way of fixing the issue is to use floatval method for 'value' key to make sure you get the right type.
I have a table like this
month | rate | admin_id
0 | 1000 | 1
I am storing data with following method:
public function store(Request $request)
{
foreach ($request->admin_id as $key => $val){
$adminbillings = AdminBilling::create([
'month' => 0,
'rate' => $request->rate[$key],
'admin_id' => $val,
]);
}
return redirect('/');
}
When I add a new row in the table using this method, I also want to replace the month of previous row (which is 0) with the timestamp of the new line. So the table will become
month | rate | admin_id
29-08-2017 | 1000 | 1
0 | 2000 | 1
I can add multiple lines from single method, but here one is store and one is update, and by default update requires two arguments. But may be I am mistaken somewhere. Can you please guide me how to achieve my target?
I can access the previous row inside the method using this line:
$ended_at = AdminBilling::where('admin_id', $val)->where('month', 0)->get();
This is not best solution but you will achieve your target.
To update previous record after insertion , you can do like this -
$res = AdminBilling::select('id')->orderBy('id','desc')->skip(1)->take(1)->first(); //to get prev. row
AdminBilling::where('id',$res->id)->update(['month',date()]); // to update month
put these lines in your store function as -
public function store(Request $request)
{
foreach ($request->admin_id as $key => $val){
$adminbillings = AdminBilling::create([
'month' => 0,
'rate' => $request->rate[$key],
'admin_id' => $val,
]);
$res = AdminBilling::select('id')->orderBy('id','desc')->skip(1)->take(1)->first();
AdminBilling::where('id',$res->id)->update(['month',date()]);
}
return redirect('/');
}
Hope this will helpful for you.
When you use create method it returns the instance of your inserted row. So you can use $adminbillings->id if you need the access to previously insterted row.
So I know there are articles out there about custom ordering but I think my situation may be a little different (Or could just be my workaround sucks :P ). Here is the quick scenario.
I pull XML data from curl off a report url that we have. (not properly formatted!) So let's use the following example: I get the following data back in one long list.
<Site>My Sites</Site>
<Customer>PHPSTEVE</Customer>
<Name>Griswald</Name>
<Site> Evil Dead</Site>
<Customer>Bruce</Customer>
<Name>Campbell</Name>
I can't use the XML function because the format header is incorrect so I won't get into that.. So instead I use a preg_match_all
preg_match_all("/<Site>(.*?)<\/Site>|<Customer>(.*?)<\/Customer>|<Name>(.*?)<\/Name>/is", $resp, $output_array[]);
Then I go through and I know this is not good but it works..
foreach(array_reverse($output_array[0][0]) as $info){
$myinfo = $info . $myinfo;
}
Everything works as I use a str_replace and just put in my table tags etc.. Obviously I'm doing the reading in reverse.. it's ok I want it that way for this.
Site | Customer | Name
----------+----------+------
Evil Dead | Bruce | Campbell
What if I want "Customer" field first? So basically I'm changing the horizontal order of Site and Customer etc... Remember there are multiple records all in a line so I am trying to figure out how I would move Customer to where I want it.. or any field for that matter. Based off of my code above, I don't believe I can accomplish it the way I want to so I'm not sure of another way to do it.
I would like to report it as this:
Customer | Site | Name
---------+-----------+------
Bruce | Evil Dead | Campbell
Any help would be appreciated. NOTE: I don't care about reading bottom up or top down.. just field changes. Thanks
UPDATE: The XMl issue I run into is:
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<results>
<Site>My Sites</Site>
<Customer>PHPSTEVE</Customer>
<Name>Griswald</Name>
<Site> Evil Dead</Site>
<Customer>Bruce</Customer>
<Name>Campbell</Name>
</results>
Maybe something like this?
function sortCustomerKeys($a, $b)
{
if ( $a == 'Customer' && $b != 'Customer' ) {
return 1;
} else if ( $a != 'Customer' && $b == 'Customer' ) {
return -1;
}
return strnatcasecmp($a, $b);
}
$customers = array();
$advance = array(
'Customer' => 0,
'Name' => 0,
'Site' => 0
);
if ( preg_match_all('#<(Customer|Name|Site)>\\s*(.*?)\\s*</\\1>#is', $resp, $m) ) {
for ($i = 0, $j = 0; $i < count($m[1]); $i++) {
$tag = $m[1][$i];
$value = $m[2][$i];
$customers[$j][$tag] = $value;
$advance[$tag] = 1;
if ( array_sum($advance) == count($advance) ) {
$advance = array(
'Customer' => 0,
'Name' => 0,
'Site' => 0
);
uksort($customers[$j], 'sortCustomerKeys');
$j++;
}
}
}
var_dump($customers);
I have two very similar classes. Lets say Class A and Class B.
+---------------+ +---------------+
| Class A | | Class B |
|---------------| |---------------|
| Name | | Name |
| ZIP | | ZIP |
| TelPhone | | TelPhone |
| | | MobilePhone |
+---------------+ +---------------+
I want to compare them in the values for all common attributes.
Here's a way I tried it, but doing it for all attributes (got more than only 3 attributes) looks like a overkill for me:
$differences = array();
if($classA->getName() != $classB->getName()) {
array_push(
$differences,
array('name' => array(
'classA' => $classA->getName(),
'classB' => $classB->getName()
)
));
}
// and the same code for every attribute....
What's the best approach here?
Additional to the handiwork, it is also not automatically updated if the classes are getting altered. For example if Class A gets also a MobilePhone attribute.
Please don't tell me, that I should do some Polymorphism, it's just an example to clarify.
I'm interested in the difference, so not only the attributes, also the values itself inside the attributes.
thanks
It's not the sexiest thing (because of the substring to getters.., but I can't go with properties, I think sym, but I got it:
// getting an array with all getter methods of a class
private function getGettersOf($object) {
$methodArray = array();
$reflection = new ReflectionClass($object);
$methods = $reflection->getMethods();
foreach ($methods as $method) {
// nur getter
if(strtolower(substr($method->getName(), 0, 3)) == "get") {
array_push($methodArray, $method->getName());
}
}
return $methodArray;
}
// getting an array with all differences
public function getDifferences($classA, $classB) {
// get list of attributes (getter methods) in commond
$classAMethods = $this->getGettersOf($classA);
$classBMethods = $this->getGettersOf($classB);
$intersection = array_intersect($classAMethods, $classBMethods);
$differences = array();
// iterate over all methods and check for the values of the reflected methods
foreach($intersection as $method) {
$refMethodClassB = new ReflectionMethod($classB, $method);
$refMethodClassA = new ReflectionMethod($classA, $method);
if($refMethodClassB->invoke($classB) != $refMethodClassA->invoke($classA)) {
array_push(
$differences,
array($method => array(
'classA' => $refMethodClassA->invoke($classA),
'classB' => $refMethodClassB->invoke($classB)
)
));
}
}
return $differences;
}
Special thanks to George Marques's comment.