api de gestion de ticket, basé sur php-crud-api. Le but est de décorrélé les outils de gestion des données, afin
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

api.php 185KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595
  1. <?php
  2. /**
  3. * PHP-CRUD-API v2 License: MIT
  4. * Maurits van der Schee: maurits@vdschee.nl
  5. * https://github.com/mevdschee/php-crud-api
  6. **/
  7. namespace Tqdev\PhpCrudApi;
  8. // file: src/Tqdev/PhpCrudApi/Cache/Cache.php
  9. interface Cache
  10. {
  11. public function set(String $key, String $value, int $ttl = 0): bool;
  12. public function get(String $key): String;
  13. public function clear(): bool;
  14. }
  15. // file: src/Tqdev/PhpCrudApi/Cache/CacheFactory.php
  16. class CacheFactory
  17. {
  18. const PREFIX = 'phpcrudapi-%s-%s-%s-';
  19. private static function getPrefix(Config $config): String
  20. {
  21. $driver = $config->getDriver();
  22. $database = $config->getDatabase();
  23. $filehash = substr(md5(__FILE__), 0, 8);
  24. return sprintf(self::PREFIX, $driver, $database, $filehash);
  25. }
  26. public static function create(Config $config): Cache
  27. {
  28. switch ($config->getCacheType()) {
  29. case 'TempFile':
  30. $cache = new TempFileCache(self::getPrefix($config), $config->getCachePath());
  31. break;
  32. case 'Redis':
  33. $cache = new RedisCache(self::getPrefix($config), $config->getCachePath());
  34. break;
  35. case 'Memcache':
  36. $cache = new MemcacheCache(self::getPrefix($config), $config->getCachePath());
  37. break;
  38. case 'Memcached':
  39. $cache = new MemcachedCache(self::getPrefix($config), $config->getCachePath());
  40. break;
  41. default:
  42. $cache = new NoCache();
  43. }
  44. return $cache;
  45. }
  46. }
  47. // file: src/Tqdev/PhpCrudApi/Cache/MemcacheCache.php
  48. class MemcacheCache implements Cache
  49. {
  50. protected $prefix;
  51. protected $memcache;
  52. public function __construct(String $prefix, String $config)
  53. {
  54. $this->prefix = $prefix;
  55. if ($config == '') {
  56. $address = 'localhost';
  57. $port = 11211;
  58. } elseif (strpos($config, ':') === false) {
  59. $address = $config;
  60. $port = 11211;
  61. } else {
  62. list($address, $port) = explode(':', $config);
  63. }
  64. $this->memcache = $this->create();
  65. $this->memcache->addServer($address, $port);
  66. }
  67. protected function create(): stdClass
  68. {
  69. return new \Memcache();
  70. }
  71. public function set(String $key, String $value, int $ttl = 0): bool
  72. {
  73. return $this->memcache->set($this->prefix . $key, $value, 0, $ttl);
  74. }
  75. public function get(String $key): String
  76. {
  77. return $this->memcache->get($this->prefix . $key) ?: '';
  78. }
  79. public function clear(): bool
  80. {
  81. return $this->memcache->flush();
  82. }
  83. }
  84. // file: src/Tqdev/PhpCrudApi/Cache/MemcachedCache.php
  85. class MemcachedCache extends MemcacheCache
  86. {
  87. protected function create(): stdClass
  88. {
  89. return new \Memcached();
  90. }
  91. public function set(String $key, String $value, int $ttl = 0): bool
  92. {
  93. return $this->memcache->set($this->prefix . $key, $value, $ttl);
  94. }
  95. }
  96. // file: src/Tqdev/PhpCrudApi/Cache/NoCache.php
  97. class NoCache implements Cache
  98. {
  99. public function __construct()
  100. {
  101. }
  102. public function set(String $key, String $value, int $ttl = 0): bool
  103. {
  104. return true;
  105. }
  106. public function get(String $key): String
  107. {
  108. return '';
  109. }
  110. public function clear(): bool
  111. {
  112. return true;
  113. }
  114. }
  115. // file: src/Tqdev/PhpCrudApi/Cache/RedisCache.php
  116. class RedisCache implements Cache
  117. {
  118. protected $prefix;
  119. protected $redis;
  120. public function __construct(String $prefix, String $config)
  121. {
  122. $this->prefix = $prefix;
  123. if ($config == '') {
  124. $config = '127.0.0.1';
  125. }
  126. $params = explode(':', $config, 6);
  127. if (isset($params[3])) {
  128. $params[3] = null;
  129. }
  130. $this->redis = new \Redis();
  131. call_user_func_array(array($this->redis, 'pconnect'), $params);
  132. }
  133. public function set(String $key, String $value, int $ttl = 0): bool
  134. {
  135. return $this->redis->set($this->prefix . $key, $value, $ttl);
  136. }
  137. public function get(String $key): String
  138. {
  139. return $this->redis->get($this->prefix . $key) ?: '';
  140. }
  141. public function clear(): bool
  142. {
  143. return $this->redis->flushDb();
  144. }
  145. }
  146. // file: src/Tqdev/PhpCrudApi/Cache/TempFileCache.php
  147. class TempFileCache implements Cache
  148. {
  149. const SUFFIX = 'cache';
  150. private $path;
  151. private $segments;
  152. public function __construct(String $prefix, String $config)
  153. {
  154. $this->segments = [];
  155. $s = DIRECTORY_SEPARATOR;
  156. $ps = PATH_SEPARATOR;
  157. if ($config == '') {
  158. $this->path = sys_get_temp_dir() . $s . $prefix . self::SUFFIX;
  159. } elseif (strpos($config, $ps) === false) {
  160. $this->path = $config;
  161. } else {
  162. list($path, $segments) = explode($ps, $config);
  163. $this->path = $path;
  164. $this->segments = explode(',', $segments);
  165. }
  166. if (file_exists($this->path) && is_dir($this->path)) {
  167. $this->clean($this->path, array_filter($this->segments), strlen(md5('')), false);
  168. }
  169. }
  170. private function getFileName(String $key): String
  171. {
  172. $s = DIRECTORY_SEPARATOR;
  173. $md5 = md5($key);
  174. $filename = rtrim($this->path, $s) . $s;
  175. $i = 0;
  176. foreach ($this->segments as $segment) {
  177. $filename .= substr($md5, $i, $segment) . $s;
  178. $i += $segment;
  179. }
  180. $filename .= substr($md5, $i);
  181. return $filename;
  182. }
  183. public function set(String $key, String $value, int $ttl = 0): bool
  184. {
  185. $filename = $this->getFileName($key);
  186. $dirname = dirname($filename);
  187. if (!file_exists($dirname)) {
  188. if (!mkdir($dirname, 0755, true)) {
  189. return false;
  190. }
  191. }
  192. $string = $ttl . '|' . $value;
  193. return $this->filePutContents($filename, $string) !== false;
  194. }
  195. private function filePutContents($filename, $string)
  196. {
  197. return file_put_contents($filename, $string, LOCK_EX);
  198. }
  199. private function fileGetContents($filename)
  200. {
  201. $file = fopen($filename, 'rb');
  202. if ($file === false) {
  203. return false;
  204. }
  205. $lock = flock($file, LOCK_SH);
  206. if (!$lock) {
  207. fclose($file);
  208. return false;
  209. }
  210. $string = '';
  211. while (!feof($file)) {
  212. $string .= fread($file, 8192);
  213. }
  214. flock($file, LOCK_UN);
  215. fclose($file);
  216. return $string;
  217. }
  218. private function getString($filename): String
  219. {
  220. $data = $this->fileGetContents($filename);
  221. if ($data === false) {
  222. return '';
  223. }
  224. list($ttl, $string) = explode('|', $data, 2);
  225. if ($ttl > 0 && time() - filemtime($filename) > $ttl) {
  226. return '';
  227. }
  228. return $string;
  229. }
  230. public function get(String $key): String
  231. {
  232. $filename = $this->getFileName($key);
  233. if (!file_exists($filename)) {
  234. return '';
  235. }
  236. $string = $this->getString($filename);
  237. if ($string == null) {
  238. return '';
  239. }
  240. return $string;
  241. }
  242. private function clean(String $path, array $segments, int $len, bool $all) /*: void*/
  243. {
  244. $entries = scandir($path);
  245. foreach ($entries as $entry) {
  246. if ($entry === '.' || $entry === '..') {
  247. continue;
  248. }
  249. $filename = $path . DIRECTORY_SEPARATOR . $entry;
  250. if (count($segments) == 0) {
  251. if (strlen($entry) != $len) {
  252. continue;
  253. }
  254. if (is_file($filename)) {
  255. if ($all || $this->getString($filename) == null) {
  256. unlink($filename);
  257. }
  258. }
  259. } else {
  260. if (strlen($entry) != $segments[0]) {
  261. continue;
  262. }
  263. if (is_dir($filename)) {
  264. $this->clean($filename, array_slice($segments, 1), $len - $segments[0], $all);
  265. rmdir($filename);
  266. }
  267. }
  268. }
  269. }
  270. public function clear(): bool
  271. {
  272. if (!file_exists($this->path) || !is_dir($this->path)) {
  273. return false;
  274. }
  275. $this->clean($this->path, array_filter($this->segments), strlen(md5('')), true);
  276. return true;
  277. }
  278. }
  279. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedColumn.php
  280. class ReflectedColumn implements \JsonSerializable
  281. {
  282. const DEFAULT_LENGTH = 255;
  283. const DEFAULT_PRECISION = 19;
  284. const DEFAULT_SCALE = 4;
  285. private $name;
  286. private $type;
  287. private $length;
  288. private $precision;
  289. private $scale;
  290. private $nullable;
  291. private $pk;
  292. private $fk;
  293. public function __construct(String $name, String $type, int $length, int $precision, int $scale, bool $nullable, bool $pk, String $fk)
  294. {
  295. $this->name = $name;
  296. $this->type = $type;
  297. $this->length = $length;
  298. $this->precision = $precision;
  299. $this->scale = $scale;
  300. $this->nullable = $nullable;
  301. $this->pk = $pk;
  302. $this->fk = $fk;
  303. $this->sanitize();
  304. }
  305. public static function fromReflection(GenericReflection $reflection, array $columnResult): ReflectedColumn
  306. {
  307. $name = $columnResult['COLUMN_NAME'];
  308. $length = (int) $columnResult['CHARACTER_MAXIMUM_LENGTH'];
  309. $type = $reflection->toJdbcType($columnResult['DATA_TYPE'], $length);
  310. $precision = (int) $columnResult['NUMERIC_PRECISION'];
  311. $scale = (int) $columnResult['NUMERIC_SCALE'];
  312. $nullable = in_array(strtoupper($columnResult['IS_NULLABLE']), ['TRUE', 'YES', 'T', 'Y', '1']);
  313. $pk = false;
  314. $fk = '';
  315. return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
  316. }
  317. public static function fromJson( /* object */$json): ReflectedColumn
  318. {
  319. $name = $json->name;
  320. $type = $json->type;
  321. $length = isset($json->length) ? $json->length : 0;
  322. $precision = isset($json->precision) ? $json->precision : 0;
  323. $scale = isset($json->scale) ? $json->scale : 0;
  324. $nullable = isset($json->nullable) ? $json->nullable : false;
  325. $pk = isset($json->pk) ? $json->pk : false;
  326. $fk = isset($json->fk) ? $json->fk : '';
  327. return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
  328. }
  329. private function sanitize()
  330. {
  331. $this->length = $this->hasLength() ? $this->getLength() : 0;
  332. $this->precision = $this->hasPrecision() ? $this->getPrecision() : 0;
  333. $this->scale = $this->hasScale() ? $this->getScale() : 0;
  334. }
  335. public function getName(): String
  336. {
  337. return $this->name;
  338. }
  339. public function getNullable(): bool
  340. {
  341. return $this->nullable;
  342. }
  343. public function getType(): String
  344. {
  345. return $this->type;
  346. }
  347. public function getLength(): int
  348. {
  349. return $this->length ?: self::DEFAULT_LENGTH;
  350. }
  351. public function getPrecision(): int
  352. {
  353. return $this->precision ?: self::DEFAULT_PRECISION;
  354. }
  355. public function getScale(): int
  356. {
  357. return $this->scale ?: self::DEFAULT_SCALE;
  358. }
  359. public function hasLength(): bool
  360. {
  361. return in_array($this->type, ['varchar', 'varbinary']);
  362. }
  363. public function hasPrecision(): bool
  364. {
  365. return $this->type == 'decimal';
  366. }
  367. public function hasScale(): bool
  368. {
  369. return $this->type == 'decimal';
  370. }
  371. public function isBinary(): bool
  372. {
  373. return in_array($this->type, ['blob', 'varbinary']);
  374. }
  375. public function isBoolean(): bool
  376. {
  377. return $this->type == 'boolean';
  378. }
  379. public function isGeometry(): bool
  380. {
  381. return $this->type == 'geometry';
  382. }
  383. public function setPk($value) /*: void*/
  384. {
  385. $this->pk = $value;
  386. }
  387. public function getPk(): bool
  388. {
  389. return $this->pk;
  390. }
  391. public function setFk($value) /*: void*/
  392. {
  393. $this->fk = $value;
  394. }
  395. public function getFk(): String
  396. {
  397. return $this->fk;
  398. }
  399. public function serialize()
  400. {
  401. return [
  402. 'name' => $this->name,
  403. 'type' => $this->type,
  404. 'length' => $this->length,
  405. 'precision' => $this->precision,
  406. 'scale' => $this->scale,
  407. 'nullable' => $this->nullable,
  408. 'pk' => $this->pk,
  409. 'fk' => $this->fk,
  410. ];
  411. }
  412. public function jsonSerialize()
  413. {
  414. return array_filter($this->serialize());
  415. }
  416. }
  417. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedDatabase.php
  418. class ReflectedDatabase implements \JsonSerializable
  419. {
  420. private $name;
  421. private $tableTypes;
  422. public function __construct(String $name, array $tableTypes)
  423. {
  424. $this->name = $name;
  425. $this->tableTypes = $tableTypes;
  426. }
  427. public static function fromReflection(GenericReflection $reflection): ReflectedDatabase
  428. {
  429. $name = $reflection->getDatabaseName();
  430. $tableTypes = [];
  431. foreach ($reflection->getTables() as $table) {
  432. $tableName = $table['TABLE_NAME'];
  433. $tableType = $table['TABLE_TYPE'];
  434. if (in_array($tableName, $reflection->getIgnoredTables())) {
  435. continue;
  436. }
  437. $tableTypes[$tableName] = $tableType;
  438. }
  439. return new ReflectedDatabase($name, $tableTypes);
  440. }
  441. public static function fromJson( /* object */$json): ReflectedDatabase
  442. {
  443. $name = $json->name;
  444. $tableTypes = (array) $json->tables;
  445. return new ReflectedDatabase($name, $tableTypes);
  446. }
  447. public function getName(): String
  448. {
  449. return $this->name;
  450. }
  451. public function hasTable(String $tableName): bool
  452. {
  453. return isset($this->tableTypes[$tableName]);
  454. }
  455. public function getType(String $tableName): String
  456. {
  457. return isset($this->tableTypes[$tableName]) ? $this->tableTypes[$tableName] : '';
  458. }
  459. public function getTableNames(): array
  460. {
  461. return array_keys($this->tableTypes);
  462. }
  463. public function removeTable(String $tableName): bool
  464. {
  465. if (!isset($this->tableTypes[$tableName])) {
  466. return false;
  467. }
  468. unset($this->tableTypes[$tableName]);
  469. return true;
  470. }
  471. public function serialize()
  472. {
  473. return [
  474. 'name' => $this->name,
  475. 'tables' => $this->tableTypes,
  476. ];
  477. }
  478. public function jsonSerialize()
  479. {
  480. return $this->serialize();
  481. }
  482. }
  483. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedTable.php
  484. class ReflectedTable implements \JsonSerializable
  485. {
  486. private $name;
  487. private $type;
  488. private $columns;
  489. private $pk;
  490. private $fks;
  491. public function __construct(String $name, String $type, array $columns)
  492. {
  493. $this->name = $name;
  494. $this->type = $type;
  495. $this->columns = [];
  496. foreach ($columns as $column) {
  497. $columnName = $column->getName();
  498. $this->columns[$columnName] = $column;
  499. }
  500. $this->pk = null;
  501. foreach ($columns as $column) {
  502. if ($column->getPk() == true) {
  503. $this->pk = $column;
  504. }
  505. }
  506. $this->fks = [];
  507. foreach ($columns as $column) {
  508. $columnName = $column->getName();
  509. $referencedTableName = $column->getFk();
  510. if ($referencedTableName != '') {
  511. $this->fks[$columnName] = $referencedTableName;
  512. }
  513. }
  514. }
  515. public static function fromReflection(GenericReflection $reflection, String $name, String $type): ReflectedTable
  516. {
  517. $columns = [];
  518. foreach ($reflection->getTableColumns($name, $type) as $tableColumn) {
  519. $column = ReflectedColumn::fromReflection($reflection, $tableColumn);
  520. $columns[$column->getName()] = $column;
  521. }
  522. $columnNames = $reflection->getTablePrimaryKeys($name);
  523. if (count($columnNames) == 1) {
  524. $columnName = $columnNames[0];
  525. if (isset($columns[$columnName])) {
  526. $pk = $columns[$columnName];
  527. $pk->setPk(true);
  528. }
  529. }
  530. $fks = $reflection->getTableForeignKeys($name);
  531. foreach ($fks as $columnName => $table) {
  532. $columns[$columnName]->setFk($table);
  533. }
  534. return new ReflectedTable($name, $type, array_values($columns));
  535. }
  536. public static function fromJson( /* object */$json): ReflectedTable
  537. {
  538. $name = $json->name;
  539. $type = $json->type;
  540. $columns = [];
  541. if (isset($json->columns) && is_array($json->columns)) {
  542. foreach ($json->columns as $column) {
  543. $columns[] = ReflectedColumn::fromJson($column);
  544. }
  545. }
  546. return new ReflectedTable($name, $type, $columns);
  547. }
  548. public function hasColumn(String $columnName): bool
  549. {
  550. return isset($this->columns[$columnName]);
  551. }
  552. public function hasPk(): bool
  553. {
  554. return $this->pk != null;
  555. }
  556. public function getPk() /*: ?ReflectedColumn */
  557. {
  558. return $this->pk;
  559. }
  560. public function getName(): String
  561. {
  562. return $this->name;
  563. }
  564. public function getType(): String
  565. {
  566. return $this->type;
  567. }
  568. public function columnNames(): array
  569. {
  570. return array_keys($this->columns);
  571. }
  572. public function getColumn($columnName): ReflectedColumn
  573. {
  574. return $this->columns[$columnName];
  575. }
  576. public function getFksTo(String $tableName): array
  577. {
  578. $columns = array();
  579. foreach ($this->fks as $columnName => $referencedTableName) {
  580. if ($tableName == $referencedTableName) {
  581. $columns[] = $this->columns[$columnName];
  582. }
  583. }
  584. return $columns;
  585. }
  586. public function removeColumn(String $columnName): bool
  587. {
  588. if (!isset($this->columns[$columnName])) {
  589. return false;
  590. }
  591. unset($this->columns[$columnName]);
  592. return true;
  593. }
  594. public function serialize()
  595. {
  596. return [
  597. 'name' => $this->name,
  598. 'type' => $this->type,
  599. 'columns' => array_values($this->columns),
  600. ];
  601. }
  602. public function jsonSerialize()
  603. {
  604. return $this->serialize();
  605. }
  606. }
  607. // file: src/Tqdev/PhpCrudApi/Column/DefinitionService.php
  608. class DefinitionService
  609. {
  610. private $db;
  611. private $reflection;
  612. public function __construct(GenericDB $db, ReflectionService $reflection)
  613. {
  614. $this->db = $db;
  615. $this->reflection = $reflection;
  616. }
  617. public function updateTable(String $tableName, /* object */ $changes): bool
  618. {
  619. $table = $this->reflection->getTable($tableName);
  620. $newTable = ReflectedTable::fromJson((object) array_merge((array) $table->jsonSerialize(), (array) $changes));
  621. if ($table->getName() != $newTable->getName()) {
  622. if (!$this->db->definition()->renameTable($table->getName(), $newTable->getName())) {
  623. return false;
  624. }
  625. }
  626. return true;
  627. }
  628. public function updateColumn(String $tableName, String $columnName, /* object */ $changes): bool
  629. {
  630. $table = $this->reflection->getTable($tableName);
  631. $column = $table->getColumn($columnName);
  632. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  633. if ($newColumn->getPk() != $column->getPk() && $table->hasPk()) {
  634. $oldColumn = $table->getPk();
  635. if ($oldColumn->getName() != $columnName) {
  636. $oldColumn->setPk(false);
  637. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $oldColumn->getName(), $oldColumn)) {
  638. return false;
  639. }
  640. }
  641. }
  642. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), ['pk' => false, 'fk' => false]));
  643. if ($newColumn->getPk() != $column->getPk() && !$newColumn->getPk()) {
  644. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $column->getName(), $newColumn)) {
  645. return false;
  646. }
  647. }
  648. if ($newColumn->getFk() != $column->getFk() && !$newColumn->getFk()) {
  649. if (!$this->db->definition()->removeColumnForeignKey($table->getName(), $column->getName(), $newColumn)) {
  650. return false;
  651. }
  652. }
  653. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  654. $newColumn->setPk(false);
  655. $newColumn->setFk('');
  656. if ($newColumn->getName() != $column->getName()) {
  657. if (!$this->db->definition()->renameColumn($table->getName(), $column->getName(), $newColumn)) {
  658. return false;
  659. }
  660. }
  661. if ($newColumn->getType() != $column->getType() ||
  662. $newColumn->getLength() != $column->getLength() ||
  663. $newColumn->getPrecision() != $column->getPrecision() ||
  664. $newColumn->getScale() != $column->getScale()
  665. ) {
  666. if (!$this->db->definition()->retypeColumn($table->getName(), $newColumn->getName(), $newColumn)) {
  667. return false;
  668. }
  669. }
  670. if ($newColumn->getNullable() != $column->getNullable()) {
  671. if (!$this->db->definition()->setColumnNullable($table->getName(), $newColumn->getName(), $newColumn)) {
  672. return false;
  673. }
  674. }
  675. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  676. if ($newColumn->getFk()) {
  677. if (!$this->db->definition()->addColumnForeignKey($table->getName(), $newColumn->getName(), $newColumn)) {
  678. return false;
  679. }
  680. }
  681. if ($newColumn->getPk()) {
  682. if (!$this->db->definition()->addColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
  683. return false;
  684. }
  685. }
  686. return true;
  687. }
  688. public function addTable( /* object */$definition)
  689. {
  690. $newTable = ReflectedTable::fromJson($definition);
  691. if (!$this->db->definition()->addTable($newTable)) {
  692. return false;
  693. }
  694. return true;
  695. }
  696. public function addColumn(String $tableName, /* object */ $definition)
  697. {
  698. $newColumn = ReflectedColumn::fromJson($definition);
  699. if (!$this->db->definition()->addColumn($tableName, $newColumn)) {
  700. return false;
  701. }
  702. if ($newColumn->getFk()) {
  703. if (!$this->db->definition()->addColumnForeignKey($tableName, $newColumn->getName(), $newColumn)) {
  704. return false;
  705. }
  706. }
  707. if ($newColumn->getPk()) {
  708. if (!$this->db->definition()->addColumnPrimaryKey($tableName, $newColumn->getName(), $newColumn)) {
  709. return false;
  710. }
  711. }
  712. return true;
  713. }
  714. public function removeTable(String $tableName)
  715. {
  716. if (!$this->db->definition()->removeTable($tableName)) {
  717. return false;
  718. }
  719. return true;
  720. }
  721. public function removeColumn(String $tableName, String $columnName)
  722. {
  723. $table = $this->reflection->getTable($tableName);
  724. $newColumn = $table->getColumn($columnName);
  725. if ($newColumn->getPk()) {
  726. $newColumn->setPk(false);
  727. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
  728. return false;
  729. }
  730. }
  731. if ($newColumn->getFk()) {
  732. $newColumn->setFk("");
  733. if (!$this->db->definition()->removeColumnForeignKey($tableName, $columnName, $newColumn)) {
  734. return false;
  735. }
  736. }
  737. if (!$this->db->definition()->removeColumn($tableName, $columnName)) {
  738. return false;
  739. }
  740. return true;
  741. }
  742. }
  743. // file: src/Tqdev/PhpCrudApi/Column/ReflectionService.php
  744. class ReflectionService
  745. {
  746. private $db;
  747. private $cache;
  748. private $ttl;
  749. private $database;
  750. private $tables;
  751. public function __construct(GenericDB $db, Cache $cache, int $ttl)
  752. {
  753. $this->db = $db;
  754. $this->cache = $cache;
  755. $this->ttl = $ttl;
  756. $this->database = $this->loadDatabase(true);
  757. $this->tables = [];
  758. }
  759. private function loadDatabase(bool $useCache): ReflectedDatabase
  760. {
  761. $data = $useCache ? $this->cache->get('ReflectedDatabase') : '';
  762. if ($data != '') {
  763. $database = ReflectedDatabase::fromJson(json_decode(gzuncompress($data)));
  764. } else {
  765. $database = ReflectedDatabase::fromReflection($this->db->reflection());
  766. $data = gzcompress(json_encode($database, JSON_UNESCAPED_UNICODE));
  767. $this->cache->set('ReflectedDatabase', $data, $this->ttl);
  768. }
  769. return $database;
  770. }
  771. private function loadTable(String $tableName, bool $useCache): ReflectedTable
  772. {
  773. $data = $useCache ? $this->cache->get("ReflectedTable($tableName)") : '';
  774. if ($data != '') {
  775. $table = ReflectedTable::fromJson(json_decode(gzuncompress($data)));
  776. } else {
  777. $tableType = $this->database->getType($tableName);
  778. $table = ReflectedTable::fromReflection($this->db->reflection(), $tableName, $tableType);
  779. $data = gzcompress(json_encode($table, JSON_UNESCAPED_UNICODE));
  780. $this->cache->set("ReflectedTable($tableName)", $data, $this->ttl);
  781. }
  782. return $table;
  783. }
  784. public function refreshTables()
  785. {
  786. $this->database = $this->loadDatabase(false);
  787. }
  788. public function refreshTable(String $tableName)
  789. {
  790. $this->tables[$tableName] = $this->loadTable($tableName, false);
  791. }
  792. public function hasTable(String $tableName): bool
  793. {
  794. return $this->database->hasTable($tableName);
  795. }
  796. public function getType(String $tableName): String
  797. {
  798. return $this->database->getType($tableName);
  799. }
  800. public function getTable(String $tableName): ReflectedTable
  801. {
  802. if (!isset($this->tables[$tableName])) {
  803. $this->tables[$tableName] = $this->loadTable($tableName, true);
  804. }
  805. return $this->tables[$tableName];
  806. }
  807. public function getTableNames(): array
  808. {
  809. return $this->database->getTableNames();
  810. }
  811. public function getDatabaseName(): String
  812. {
  813. return $this->database->getName();
  814. }
  815. public function removeTable(String $tableName): bool
  816. {
  817. unset($this->tables[$tableName]);
  818. return $this->database->removeTable($tableName);
  819. }
  820. }
  821. // file: src/Tqdev/PhpCrudApi/Controller/CacheController.php
  822. class CacheController
  823. {
  824. private $cache;
  825. private $responder;
  826. public function __construct(Router $router, Responder $responder, Cache $cache)
  827. {
  828. $router->register('GET', '/cache/clear', array($this, 'clear'));
  829. $this->cache = $cache;
  830. $this->responder = $responder;
  831. }
  832. public function clear(Request $request): Response
  833. {
  834. return $this->responder->success($this->cache->clear());
  835. }
  836. }
  837. // file: src/Tqdev/PhpCrudApi/Controller/ColumnController.php
  838. class ColumnController
  839. {
  840. private $responder;
  841. private $reflection;
  842. private $definition;
  843. public function __construct(Router $router, Responder $responder, ReflectionService $reflection, DefinitionService $definition)
  844. {
  845. $router->register('GET', '/columns', array($this, 'getDatabase'));
  846. $router->register('GET', '/columns/*', array($this, 'getTable'));
  847. $router->register('GET', '/columns/*/*', array($this, 'getColumn'));
  848. $router->register('PUT', '/columns/*', array($this, 'updateTable'));
  849. $router->register('PUT', '/columns/*/*', array($this, 'updateColumn'));
  850. $router->register('POST', '/columns', array($this, 'addTable'));
  851. $router->register('POST', '/columns/*', array($this, 'addColumn'));
  852. $router->register('DELETE', '/columns/*', array($this, 'removeTable'));
  853. $router->register('DELETE', '/columns/*/*', array($this, 'removeColumn'));
  854. $this->responder = $responder;
  855. $this->reflection = $reflection;
  856. $this->definition = $definition;
  857. }
  858. public function getDatabase(Request $request): Response
  859. {
  860. $name = $this->reflection->getDatabaseName();
  861. $tables = [];
  862. foreach ($this->reflection->getTableNames() as $table) {
  863. $tables[] = $this->reflection->getTable($table);
  864. }
  865. $database = ['name' => $name, 'tables' => $tables];
  866. return $this->responder->success($database);
  867. }
  868. public function getTable(Request $request): Response
  869. {
  870. $tableName = $request->getPathSegment(2);
  871. if (!$this->reflection->hasTable($tableName)) {
  872. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  873. }
  874. $table = $this->reflection->getTable($tableName);
  875. return $this->responder->success($table);
  876. }
  877. public function getColumn(Request $request): Response
  878. {
  879. $tableName = $request->getPathSegment(2);
  880. $columnName = $request->getPathSegment(3);
  881. if (!$this->reflection->hasTable($tableName)) {
  882. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  883. }
  884. $table = $this->reflection->getTable($tableName);
  885. if (!$table->hasColumn($columnName)) {
  886. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  887. }
  888. $column = $table->getColumn($columnName);
  889. return $this->responder->success($column);
  890. }
  891. public function updateTable(Request $request): Response
  892. {
  893. $tableName = $request->getPathSegment(2);
  894. if (!$this->reflection->hasTable($tableName)) {
  895. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  896. }
  897. $success = $this->definition->updateTable($tableName, $request->getBody());
  898. if ($success) {
  899. $this->reflection->refreshTables();
  900. }
  901. return $this->responder->success($success);
  902. }
  903. public function updateColumn(Request $request): Response
  904. {
  905. $tableName = $request->getPathSegment(2);
  906. $columnName = $request->getPathSegment(3);
  907. if (!$this->reflection->hasTable($tableName)) {
  908. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  909. }
  910. $table = $this->reflection->getTable($tableName);
  911. if (!$table->hasColumn($columnName)) {
  912. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  913. }
  914. $success = $this->definition->updateColumn($tableName, $columnName, $request->getBody());
  915. if ($success) {
  916. $this->reflection->refreshTable($tableName);
  917. }
  918. return $this->responder->success($success);
  919. }
  920. public function addTable(Request $request): Response
  921. {
  922. $tableName = $request->getBody()->name;
  923. if ($this->reflection->hasTable($tableName)) {
  924. return $this->responder->error(ErrorCode::TABLE_ALREADY_EXISTS, $tableName);
  925. }
  926. $success = $this->definition->addTable($request->getBody());
  927. if ($success) {
  928. $this->reflection->refreshTables();
  929. }
  930. return $this->responder->success($success);
  931. }
  932. public function addColumn(Request $request): Response
  933. {
  934. $tableName = $request->getPathSegment(2);
  935. if (!$this->reflection->hasTable($tableName)) {
  936. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  937. }
  938. $columnName = $request->getBody()->name;
  939. $table = $this->reflection->getTable($tableName);
  940. if ($table->hasColumn($columnName)) {
  941. return $this->responder->error(ErrorCode::COLUMN_ALREADY_EXISTS, $columnName);
  942. }
  943. $success = $this->definition->addColumn($tableName, $request->getBody());
  944. if ($success) {
  945. $this->reflection->refreshTable($tableName);
  946. }
  947. return $this->responder->success($success);
  948. }
  949. public function removeTable(Request $request): Response
  950. {
  951. $tableName = $request->getPathSegment(2);
  952. if (!$this->reflection->hasTable($tableName)) {
  953. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  954. }
  955. $success = $this->definition->removeTable($tableName);
  956. if ($success) {
  957. $this->reflection->refreshTables();
  958. }
  959. return $this->responder->success($success);
  960. }
  961. public function removeColumn(Request $request): Response
  962. {
  963. $tableName = $request->getPathSegment(2);
  964. $columnName = $request->getPathSegment(3);
  965. if (!$this->reflection->hasTable($tableName)) {
  966. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  967. }
  968. $table = $this->reflection->getTable($tableName);
  969. if (!$table->hasColumn($columnName)) {
  970. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  971. }
  972. $success = $this->definition->removeColumn($tableName, $columnName);
  973. if ($success) {
  974. $this->reflection->refreshTable($tableName);
  975. }
  976. return $this->responder->success($success);
  977. }
  978. }
  979. // file: src/Tqdev/PhpCrudApi/Controller/OpenApiController.php
  980. class OpenApiController
  981. {
  982. private $openApi;
  983. private $responder;
  984. public function __construct(Router $router, Responder $responder, OpenApiService $openApi)
  985. {
  986. $router->register('GET', '/openapi', array($this, 'openapi'));
  987. $this->openApi = $openApi;
  988. $this->responder = $responder;
  989. }
  990. public function openapi(Request $request): Response
  991. {
  992. return $this->responder->success($this->openApi->get());
  993. }
  994. }
  995. // file: src/Tqdev/PhpCrudApi/Controller/RecordController.php
  996. class RecordController
  997. {
  998. private $service;
  999. private $responder;
  1000. public function __construct(Router $router, Responder $responder, RecordService $service)
  1001. {
  1002. $router->register('GET', '/records/*', array($this, '_list'));
  1003. $router->register('POST', '/records/*', array($this, 'create'));
  1004. $router->register('GET', '/records/*/*', array($this, 'read'));
  1005. $router->register('PUT', '/records/*/*', array($this, 'update'));
  1006. $router->register('DELETE', '/records/*/*', array($this, 'delete'));
  1007. $router->register('PATCH', '/records/*/*', array($this, 'increment'));
  1008. $this->service = $service;
  1009. $this->responder = $responder;
  1010. }
  1011. public function _list(Request $request): Response
  1012. {
  1013. $table = $request->getPathSegment(2);
  1014. $params = $request->getParams();
  1015. if (!$this->service->hasTable($table)) {
  1016. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  1017. }
  1018. return $this->responder->success($this->service->_list($table, $params));
  1019. }
  1020. public function read(Request $request): Response
  1021. {
  1022. $table = $request->getPathSegment(2);
  1023. if (!$this->service->hasTable($table)) {
  1024. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  1025. }
  1026. if ($this->service->getType($table) != 'table') {
  1027. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  1028. }
  1029. $id = $request->getPathSegment(3);
  1030. $params = $request->getParams();
  1031. if (strpos($id, ',') !== false) {
  1032. $ids = explode(',', $id);
  1033. $result = [];
  1034. for ($i = 0; $i < count($ids); $i++) {
  1035. array_push($result, $this->service->read($table, $ids[$i], $params));
  1036. }
  1037. return $this->responder->success($result);
  1038. } else {
  1039. $response = $this->service->read($table, $id, $params);
  1040. if ($response === null) {
  1041. return $this->responder->error(ErrorCode::RECORD_NOT_FOUND, $id);
  1042. }
  1043. return $this->responder->success($response);
  1044. }
  1045. }
  1046. public function create(Request $request): Response
  1047. {
  1048. $table = $request->getPathSegment(2);
  1049. if (!$this->service->hasTable($table)) {
  1050. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  1051. }
  1052. if ($this->service->getType($table) != 'table') {
  1053. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  1054. }
  1055. $record = $request->getBody();
  1056. if ($record === null) {
  1057. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  1058. }
  1059. $params = $request->getParams();
  1060. if (is_array($record)) {
  1061. $result = array();
  1062. foreach ($record as $r) {
  1063. $result[] = $this->service->create($table, $r, $params);
  1064. }
  1065. return $this->responder->success($result);
  1066. } else {
  1067. return $this->responder->success($this->service->create($table, $record, $params));
  1068. }
  1069. }
  1070. public function update(Request $request): Response
  1071. {
  1072. $table = $request->getPathSegment(2);
  1073. if (!$this->service->hasTable($table)) {
  1074. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  1075. }
  1076. if ($this->service->getType($table) != 'table') {
  1077. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  1078. }
  1079. $id = $request->getPathSegment(3);
  1080. $params = $request->getParams();
  1081. $record = $request->getBody();
  1082. if ($record === null) {
  1083. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  1084. }
  1085. $ids = explode(',', $id);
  1086. if (is_array($record)) {
  1087. if (count($ids) != count($record)) {
  1088. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  1089. }
  1090. $result = array();
  1091. for ($i = 0; $i < count($ids); $i++) {
  1092. $result[] = $this->service->update($table, $ids[$i], $record[$i], $params);
  1093. }
  1094. return $this->responder->success($result);
  1095. } else {
  1096. if (count($ids) != 1) {
  1097. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  1098. }
  1099. return $this->responder->success($this->service->update($table, $id, $record, $params));
  1100. }
  1101. }
  1102. public function delete(Request $request): Response
  1103. {
  1104. $table = $request->getPathSegment(2);
  1105. if (!$this->service->hasTable($table)) {
  1106. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  1107. }
  1108. if ($this->service->getType($table) != 'table') {
  1109. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  1110. }
  1111. $id = $request->getPathSegment(3);
  1112. $params = $request->getParams();
  1113. $ids = explode(',', $id);
  1114. if (count($ids) > 1) {
  1115. $result = array();
  1116. for ($i = 0; $i < count($ids); $i++) {
  1117. $result[] = $this->service->delete($table, $ids[$i], $params);
  1118. }
  1119. return $this->responder->success($result);
  1120. } else {
  1121. return $this->responder->success($this->service->delete($table, $id, $params));
  1122. }
  1123. }
  1124. public function increment(Request $request): Response
  1125. {
  1126. $table = $request->getPathSegment(2);
  1127. if (!$this->service->hasTable($table)) {
  1128. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  1129. }
  1130. if ($this->service->getType($table) != 'table') {
  1131. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  1132. }
  1133. $id = $request->getPathSegment(3);
  1134. $record = $request->getBody();
  1135. if ($record === null) {
  1136. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  1137. }
  1138. $params = $request->getParams();
  1139. $ids = explode(',', $id);
  1140. if (is_array($record)) {
  1141. if (count($ids) != count($record)) {
  1142. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  1143. }
  1144. $result = array();
  1145. for ($i = 0; $i < count($ids); $i++) {
  1146. $result[] = $this->service->increment($table, $ids[$i], $record[$i], $params);
  1147. }
  1148. return $this->responder->success($result);
  1149. } else {
  1150. if (count($ids) != 1) {
  1151. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  1152. }
  1153. return $this->responder->success($this->service->increment($table, $id, $record, $params));
  1154. }
  1155. }
  1156. }
  1157. // file: src/Tqdev/PhpCrudApi/Controller/Responder.php
  1158. class Responder
  1159. {
  1160. public function error(int $error, String $argument, $details = null): Response
  1161. {
  1162. $errorCode = new ErrorCode($error);
  1163. $status = $errorCode->getStatus();
  1164. $document = new ErrorDocument($errorCode, $argument, $details);
  1165. return new Response($status, $document);
  1166. }
  1167. public function success($result): Response
  1168. {
  1169. return new Response(Response::OK, $result);
  1170. }
  1171. }
  1172. // file: src/Tqdev/PhpCrudApi/Database/ColumnConverter.php
  1173. class ColumnConverter
  1174. {
  1175. private $driver;
  1176. public function __construct(String $driver)
  1177. {
  1178. $this->driver = $driver;
  1179. }
  1180. public function convertColumnValue(ReflectedColumn $column): String
  1181. {
  1182. if ($column->isBinary()) {
  1183. switch ($this->driver) {
  1184. case 'mysql':
  1185. return "FROM_BASE64(?)";
  1186. case 'pgsql':
  1187. return "decode(?, 'base64')";
  1188. case 'sqlsrv':
  1189. return "CONVERT(XML, ?).value('.','varbinary(max)')";
  1190. }
  1191. }
  1192. if ($column->isGeometry()) {
  1193. switch ($this->driver) {
  1194. case 'mysql':
  1195. case 'pgsql':
  1196. return "ST_GeomFromText(?)";
  1197. case 'sqlsrv':
  1198. return "geometry::STGeomFromText(?,0)";
  1199. }
  1200. }
  1201. return '?';
  1202. }
  1203. public function convertColumnName(ReflectedColumn $column, $value): String
  1204. {
  1205. if ($column->isBinary()) {
  1206. switch ($this->driver) {
  1207. case 'mysql':
  1208. return "TO_BASE64($value) as $value";
  1209. case 'pgsql':
  1210. return "encode($value::bytea, 'base64') as $value";
  1211. case 'sqlsrv':
  1212. return "CASE WHEN $value IS NULL THEN NULL ELSE (SELECT CAST($value as varbinary(max)) FOR XML PATH(''), BINARY BASE64) END as $value";
  1213. }
  1214. }
  1215. if ($column->isGeometry()) {
  1216. switch ($this->driver) {
  1217. case 'mysql':
  1218. case 'pgsql':
  1219. return "ST_AsText($value) as $value";
  1220. case 'sqlsrv':
  1221. return "REPLACE($value.STAsText(),' (','(') as $value";
  1222. }
  1223. }
  1224. return $value;
  1225. }
  1226. }
  1227. // file: src/Tqdev/PhpCrudApi/Database/ColumnsBuilder.php
  1228. class ColumnsBuilder
  1229. {
  1230. private $driver;
  1231. private $converter;
  1232. public function __construct(String $driver)
  1233. {
  1234. $this->driver = $driver;
  1235. $this->converter = new ColumnConverter($driver);
  1236. }
  1237. public function getOffsetLimit(int $offset, int $limit): String
  1238. {
  1239. if ($limit < 0 || $offset < 0) {
  1240. return '';
  1241. }
  1242. switch ($this->driver) {
  1243. case 'mysql':return "LIMIT $offset, $limit";
  1244. case 'pgsql':return "LIMIT $limit OFFSET $offset";
  1245. case 'sqlsrv':return "OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY";
  1246. }
  1247. }
  1248. private function quoteColumnName(ReflectedColumn $column): String
  1249. {
  1250. return '"' . $column->getName() . '"';
  1251. }
  1252. public function getOrderBy(ReflectedTable $table, array $columnOrdering): String
  1253. {
  1254. $results = array();
  1255. foreach ($columnOrdering as $i => list($columnName, $ordering)) {
  1256. $column = $table->getColumn($columnName);
  1257. $quotedColumnName = $this->quoteColumnName($column);
  1258. $results[] = $quotedColumnName . ' ' . $ordering;
  1259. }
  1260. return implode(',', $results);
  1261. }
  1262. public function getSelect(ReflectedTable $table, array $columnNames): String
  1263. {
  1264. $results = array();
  1265. foreach ($columnNames as $columnName) {
  1266. $column = $table->getColumn($columnName);
  1267. $quotedColumnName = $this->quoteColumnName($column);
  1268. $quotedColumnName = $this->converter->convertColumnName($column, $quotedColumnName);
  1269. $results[] = $quotedColumnName;
  1270. }
  1271. return implode(',', $results);
  1272. }
  1273. public function getInsert(ReflectedTable $table, array $columnValues): String
  1274. {
  1275. $columns = array();
  1276. $values = array();
  1277. foreach ($columnValues as $columnName => $columnValue) {
  1278. $column = $table->getColumn($columnName);
  1279. $quotedColumnName = $this->quoteColumnName($column);
  1280. $columns[] = $quotedColumnName;
  1281. $columnValue = $this->converter->convertColumnValue($column);
  1282. $values[] = $columnValue;
  1283. }
  1284. $columnsSql = '(' . implode(',', $columns) . ')';
  1285. $valuesSql = '(' . implode(',', $values) . ')';
  1286. $outputColumn = $this->quoteColumnName($table->getPk());
  1287. switch ($this->driver) {
  1288. case 'mysql':return "$columnsSql VALUES $valuesSql";
  1289. case 'pgsql':return "$columnsSql VALUES $valuesSql RETURNING $outputColumn";
  1290. case 'sqlsrv':return "$columnsSql OUTPUT INSERTED.$outputColumn VALUES $valuesSql";
  1291. }
  1292. }
  1293. public function getUpdate(ReflectedTable $table, array $columnValues): String
  1294. {
  1295. $results = array();
  1296. foreach ($columnValues as $columnName => $columnValue) {
  1297. $column = $table->getColumn($columnName);
  1298. $quotedColumnName = $this->quoteColumnName($column);
  1299. $columnValue = $this->converter->convertColumnValue($column);
  1300. $results[] = $quotedColumnName . '=' . $columnValue;
  1301. }
  1302. return implode(',', $results);
  1303. }
  1304. public function getIncrement(ReflectedTable $table, array $columnValues): String
  1305. {
  1306. $results = array();
  1307. foreach ($columnValues as $columnName => $columnValue) {
  1308. if (!is_numeric($columnValue)) {
  1309. continue;
  1310. }
  1311. $column = $table->getColumn($columnName);
  1312. $quotedColumnName = $this->quoteColumnName($column);
  1313. $columnValue = $this->converter->convertColumnValue($column);
  1314. $results[] = $quotedColumnName . '=' . $quotedColumnName . '+' . $columnValue;
  1315. }
  1316. return implode(',', $results);
  1317. }
  1318. }
  1319. // file: src/Tqdev/PhpCrudApi/Database/ConditionsBuilder.php
  1320. class ConditionsBuilder
  1321. {
  1322. private $driver;
  1323. public function __construct(String $driver)
  1324. {
  1325. $this->driver = $driver;
  1326. }
  1327. private function getConditionSql(Condition $condition, array &$arguments): String
  1328. {
  1329. if ($condition instanceof AndCondition) {
  1330. return $this->getAndConditionSql($condition, $arguments);
  1331. }
  1332. if ($condition instanceof OrCondition) {
  1333. return $this->getOrConditionSql($condition, $arguments);
  1334. }
  1335. if ($condition instanceof NotCondition) {
  1336. return $this->getNotConditionSql($condition, $arguments);
  1337. }
  1338. if ($condition instanceof ColumnCondition) {
  1339. return $this->getColumnConditionSql($condition, $arguments);
  1340. }
  1341. if ($condition instanceof SpatialCondition) {
  1342. return $this->getSpatialConditionSql($condition, $arguments);
  1343. }
  1344. throw new \Exception('Unknown Condition: ' . get_class($condition));
  1345. }
  1346. private function getAndConditionSql(AndCondition $and, array &$arguments): String
  1347. {
  1348. $parts = [];
  1349. foreach ($and->getConditions() as $condition) {
  1350. $parts[] = $this->getConditionSql($condition, $arguments);
  1351. }
  1352. return '(' . implode(' AND ', $parts) . ')';
  1353. }
  1354. private function getOrConditionSql(OrCondition $or, array &$arguments): String
  1355. {
  1356. $parts = [];
  1357. foreach ($or->getConditions() as $condition) {
  1358. $parts[] = $this->getConditionSql($condition, $arguments);
  1359. }
  1360. return '(' . implode(' OR ', $parts) . ')';
  1361. }
  1362. private function getNotConditionSql(NotCondition $not, array &$arguments): String
  1363. {
  1364. $condition = $not->getCondition();
  1365. return '(NOT ' . $this->getConditionSql($condition, $arguments) . ')';
  1366. }
  1367. private function quoteColumnName(ReflectedColumn $column): String
  1368. {
  1369. return '"' . $column->getName() . '"';
  1370. }
  1371. private function escapeLikeValue(String $value): String
  1372. {
  1373. return addcslashes($value, '%_');
  1374. }
  1375. private function getColumnConditionSql(ColumnCondition $condition, array &$arguments): String
  1376. {
  1377. $column = $this->quoteColumnName($condition->getColumn());
  1378. $operator = $condition->getOperator();
  1379. $value = $condition->getValue();
  1380. switch ($operator) {
  1381. case 'cs':
  1382. $sql = "$column LIKE ?";
  1383. $arguments[] = '%' . $this->escapeLikeValue($value) . '%';
  1384. break;
  1385. case 'sw':
  1386. $sql = "$column LIKE ?";
  1387. $arguments[] = $this->escapeLikeValue($value) . '%';
  1388. break;
  1389. case 'ew':
  1390. $sql = "$column LIKE ?";
  1391. $arguments[] = '%' . $this->escapeLikeValue($value);
  1392. break;
  1393. case 'eq':
  1394. $sql = "$column = ?";
  1395. $arguments[] = $value;
  1396. break;
  1397. case 'lt':
  1398. $sql = "$column < ?";
  1399. $arguments[] = $value;
  1400. break;
  1401. case 'le':
  1402. $sql = "$column <= ?";
  1403. $arguments[] = $value;
  1404. break;
  1405. case 'ge':
  1406. $sql = "$column >= ?";
  1407. $arguments[] = $value;
  1408. break;
  1409. case 'gt':
  1410. $sql = "$column > ?";
  1411. $arguments[] = $value;
  1412. break;
  1413. case 'bt':
  1414. $parts = explode(',', $value, 2);
  1415. $count = count($parts);
  1416. if ($count == 2) {
  1417. $sql = "($column >= ? AND $column <= ?)";
  1418. $arguments[] = $parts[0];
  1419. $arguments[] = $parts[1];
  1420. } else {
  1421. $sql = "FALSE";
  1422. }
  1423. break;
  1424. case 'in':
  1425. $parts = explode(',', $value);
  1426. $count = count($parts);
  1427. if ($count > 0) {
  1428. $qmarks = implode(',', str_split(str_repeat('?', $count)));
  1429. $sql = "$column IN ($qmarks)";
  1430. for ($i = 0; $i < $count; $i++) {
  1431. $arguments[] = $parts[$i];
  1432. }
  1433. } else {
  1434. $sql = "FALSE";
  1435. }
  1436. break;
  1437. case 'is':
  1438. $sql = "$column IS NULL";
  1439. break;
  1440. }
  1441. return $sql;
  1442. }
  1443. private function getSpatialFunctionName(String $operator): String
  1444. {
  1445. switch ($operator) {
  1446. case 'co':return 'ST_Contains';
  1447. case 'cr':return 'ST_Crosses';
  1448. case 'di':return 'ST_Disjoint';
  1449. case 'eq':return 'ST_Equals';
  1450. case 'in':return 'ST_Intersects';
  1451. case 'ov':return 'ST_Overlaps';
  1452. case 'to':return 'ST_Touches';
  1453. case 'wi':return 'ST_Within';
  1454. case 'ic':return 'ST_IsClosed';
  1455. case 'is':return 'ST_IsSimple';
  1456. case 'iv':return 'ST_IsValid';
  1457. }
  1458. }
  1459. private function hasSpatialArgument(String $operator): bool
  1460. {
  1461. return in_array($operator, ['ic', 'is', 'iv']) ? false : true;
  1462. }
  1463. private function getSpatialFunctionCall(String $functionName, String $column, bool $hasArgument): String
  1464. {
  1465. switch ($this->driver) {
  1466. case 'mysql':
  1467. case 'pgsql':
  1468. $argument = $hasArgument ? 'ST_GeomFromText(?)' : '';
  1469. return "$functionName($column, $argument)=TRUE";
  1470. case 'sql_srv':
  1471. $functionName = str_replace('ST_', 'ST', $functionName);
  1472. $argument = $hasArgument ? 'geometry::STGeomFromText(?,0)' : '';
  1473. return "$column.$functionName($argument)=1";
  1474. }
  1475. }
  1476. private function getSpatialConditionSql(ColumnCondition $condition, array &$arguments): String
  1477. {
  1478. $column = $this->quoteColumnName($condition->getColumn());
  1479. $operator = $condition->getOperator();
  1480. $value = $condition->getValue();
  1481. $functionName = $this->getSpatialFunctionName($operator);
  1482. $hasArgument = $this->hasSpatialArgument($operator);
  1483. $sql = $this->getSpatialFunctionCall($functionName, $column, $hasArgument);
  1484. if ($hasArgument) {
  1485. $arguments[] = $value;
  1486. }
  1487. return $sql;
  1488. }
  1489. public function getWhereClause(Condition $condition, array &$arguments): String
  1490. {
  1491. if ($condition instanceof NoCondition) {
  1492. return '';
  1493. }
  1494. return ' WHERE ' . $this->getConditionSql($condition, $arguments);
  1495. }
  1496. }
  1497. // file: src/Tqdev/PhpCrudApi/Database/DataConverter.php
  1498. class DataConverter
  1499. {
  1500. private $driver;
  1501. public function __construct(String $driver)
  1502. {
  1503. $this->driver = $driver;
  1504. }
  1505. private function convertRecordValue($conversion, $value)
  1506. {
  1507. switch ($conversion) {
  1508. case 'boolean':
  1509. return $value ? true : false;
  1510. }
  1511. return $value;
  1512. }
  1513. private function getRecordValueConversion(ReflectedColumn $column): String
  1514. {
  1515. if (in_array($this->driver, ['mysql', 'sqlsrv']) && $column->isBoolean()) {
  1516. return 'boolean';
  1517. }
  1518. return 'none';
  1519. }
  1520. public function convertRecords(ReflectedTable $table, array $columnNames, array &$records) /*: void*/
  1521. {
  1522. foreach ($columnNames as $columnName) {
  1523. $column = $table->getColumn($columnName);
  1524. $conversion = $this->getRecordValueConversion($column);
  1525. if ($conversion != 'none') {
  1526. foreach ($records as $i => $record) {
  1527. $value = $records[$i][$columnName];
  1528. if ($value === null) {
  1529. continue;
  1530. }
  1531. $records[$i][$columnName] = $this->convertRecordValue($conversion, $value);
  1532. }
  1533. }
  1534. }
  1535. }
  1536. private function convertInputValue($conversion, $value)
  1537. {
  1538. switch ($conversion) {
  1539. case 'base64url_to_base64':
  1540. return str_pad(strtr($value, '-_', '+/'), ceil(strlen($value) / 4) * 4, '=', STR_PAD_RIGHT);
  1541. }
  1542. return $value;
  1543. }
  1544. private function getInputValueConversion(ReflectedColumn $column): String
  1545. {
  1546. if ($column->isBinary()) {
  1547. return 'base64url_to_base64';
  1548. }
  1549. return 'none';
  1550. }
  1551. public function convertColumnValues(ReflectedTable $table, array &$columnValues) /*: void*/
  1552. {
  1553. $columnNames = array_keys($columnValues);
  1554. foreach ($columnNames as $columnName) {
  1555. $column = $table->getColumn($columnName);
  1556. $conversion = $this->getInputValueConversion($column);
  1557. if ($conversion != 'none') {
  1558. $value = $columnValues[$columnName];
  1559. if ($value !== null) {
  1560. $columnValues[$columnName] = $this->convertInputValue($conversion, $value);
  1561. }
  1562. }
  1563. }
  1564. }
  1565. }
  1566. // file: src/Tqdev/PhpCrudApi/Database/GenericDB.php
  1567. class GenericDB
  1568. {
  1569. private $driver;
  1570. private $database;
  1571. private $pdo;
  1572. private $reflection;
  1573. private $definition;
  1574. private $conditions;
  1575. private $columns;
  1576. private $converter;
  1577. private function getDsn(String $address, String $port = null, String $database = null): String
  1578. {
  1579. switch ($this->driver) {
  1580. case 'mysql':return "$this->driver:host=$address;port=$port;dbname=$database;charset=utf8mb4";
  1581. case 'pgsql':return "$this->driver:host=$address port=$port dbname=$database options='--client_encoding=UTF8'";
  1582. case 'sqlsrv':return "$this->driver:Server=$address,$port;Database=$database";
  1583. }
  1584. }
  1585. private function getCommands(): array
  1586. {
  1587. switch ($this->driver) {
  1588. case 'mysql':return [
  1589. 'SET SESSION sql_warnings=1;',
  1590. 'SET NAMES utf8mb4;',
  1591. 'SET SESSION sql_mode = "ANSI,TRADITIONAL";',
  1592. ];
  1593. case 'pgsql':return [
  1594. "SET NAMES 'UTF8';",
  1595. ];
  1596. case 'sqlsrv':return [
  1597. ];
  1598. }
  1599. }
  1600. private function getOptions(): array
  1601. {
  1602. $options = array(
  1603. \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
  1604. \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
  1605. );
  1606. switch ($this->driver) {
  1607. case 'mysql':return $options + [
  1608. \PDO::ATTR_EMULATE_PREPARES => false,
  1609. \PDO::MYSQL_ATTR_FOUND_ROWS => true,
  1610. \PDO::ATTR_PERSISTENT => true,
  1611. ];
  1612. case 'pgsql':return $options + [
  1613. \PDO::ATTR_EMULATE_PREPARES => false,
  1614. \PDO::ATTR_PERSISTENT => true,
  1615. ];
  1616. case 'sqlsrv':return $options + [
  1617. \PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
  1618. ];
  1619. }
  1620. }
  1621. public function __construct(String $driver, String $address, String $port = null, String $database = null, String $username = null, String $password = null)
  1622. {
  1623. $this->driver = $driver;
  1624. $this->database = $database;
  1625. $dsn = $this->getDsn($address, $port, $database);
  1626. $options = $this->getOptions();
  1627. $this->pdo = new \PDO($dsn, $username, $password, $options);
  1628. $commands = $this->getCommands();
  1629. foreach ($commands as $command) {
  1630. $this->pdo->query($command);
  1631. }
  1632. $this->reflection = new GenericReflection($this->pdo, $driver, $database);
  1633. $this->definition = new GenericDefinition($this->pdo, $driver, $database);
  1634. $this->conditions = new ConditionsBuilder($driver);
  1635. $this->columns = new ColumnsBuilder($driver);
  1636. $this->converter = new DataConverter($driver);
  1637. }
  1638. public function pdo(): \PDO
  1639. {
  1640. return $this->pdo;
  1641. }
  1642. public function reflection(): GenericReflection
  1643. {
  1644. return $this->reflection;
  1645. }
  1646. public function definition(): GenericDefinition
  1647. {
  1648. return $this->definition;
  1649. }
  1650. private function addMiddlewareConditions(String $tableName, Condition $condition): Condition
  1651. {
  1652. $condition1 = VariableStore::get("authorization.conditions.$tableName");
  1653. if ($condition1) {
  1654. $condition = $condition->_and($condition1);
  1655. }
  1656. $condition2 = VariableStore::get("multiTenancy.conditions.$tableName");
  1657. if ($condition2) {
  1658. $condition = $condition->_and($condition2);
  1659. }
  1660. return $condition;
  1661. }
  1662. public function createSingle(ReflectedTable $table, array $columnValues) /*: ?String*/
  1663. {
  1664. $this->converter->convertColumnValues($table, $columnValues);
  1665. $insertColumns = $this->columns->getInsert($table, $columnValues);
  1666. $tableName = $table->getName();
  1667. $pkName = $table->getPk()->getName();
  1668. $parameters = array_values($columnValues);
  1669. $sql = 'INSERT INTO "' . $tableName . '" ' . $insertColumns;
  1670. $stmt = $this->query($sql, $parameters);
  1671. if (isset($columnValues[$pkName])) {
  1672. return $columnValues[$pkName];
  1673. }
  1674. switch ($this->driver) {
  1675. case 'mysql':
  1676. $stmt = $this->query('SELECT LAST_INSERT_ID()', []);
  1677. break;
  1678. }
  1679. return $stmt->fetchColumn(0);
  1680. }
  1681. public function selectSingle(ReflectedTable $table, array $columnNames, String $id) /*: ?array*/
  1682. {
  1683. $selectColumns = $this->columns->getSelect($table, $columnNames);
  1684. $tableName = $table->getName();
  1685. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  1686. $condition = $this->addMiddlewareConditions($tableName, $condition);
  1687. $parameters = array();
  1688. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  1689. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
  1690. $stmt = $this->query($sql, $parameters);
  1691. $record = $stmt->fetch() ?: null;
  1692. if ($record === null) {
  1693. return null;
  1694. }
  1695. $records = array($record);
  1696. $this->converter->convertRecords($table, $columnNames, $records);
  1697. return $records[0];
  1698. }
  1699. public function selectMultiple(ReflectedTable $table, array $columnNames, array $ids): array
  1700. {
  1701. if (count($ids) == 0) {
  1702. return [];
  1703. }
  1704. $selectColumns = $this->columns->getSelect($table, $columnNames);
  1705. $tableName = $table->getName();
  1706. $condition = new ColumnCondition($table->getPk(), 'in', implode(',', $ids));
  1707. $condition = $this->addMiddlewareConditions($tableName, $condition);
  1708. $parameters = array();
  1709. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  1710. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
  1711. $stmt = $this->query($sql, $parameters);
  1712. $records = $stmt->fetchAll();
  1713. $this->converter->convertRecords($table, $columnNames, $records);
  1714. return $records;
  1715. }
  1716. public function selectCount(ReflectedTable $table, Condition $condition): int
  1717. {
  1718. $tableName = $table->getName();
  1719. $condition = $this->addMiddlewareConditions($tableName, $condition);
  1720. $parameters = array();
  1721. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  1722. $sql = 'SELECT COUNT(*) FROM "' . $tableName . '"' . $whereClause;
  1723. $stmt = $this->query($sql, $parameters);
  1724. return $stmt->fetchColumn(0);
  1725. }
  1726. public function selectAllUnordered(ReflectedTable $table, array $columnNames, Condition $condition): array
  1727. {
  1728. $selectColumns = $this->columns->getSelect($table, $columnNames);
  1729. $tableName = $table->getName();
  1730. $condition = $this->addMiddlewareConditions($tableName, $condition);
  1731. $parameters = array();
  1732. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  1733. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '"' . $whereClause;
  1734. $stmt = $this->query($sql, $parameters);
  1735. $records = $stmt->fetchAll();
  1736. $this->converter->convertRecords($table, $columnNames, $records);
  1737. return $records;
  1738. }
  1739. public function selectAll(ReflectedTable $table, array $columnNames, Condition $condition, array $columnOrdering, int $offset, int $limit): array
  1740. {
  1741. if ($limit == 0) {
  1742. return array();
  1743. }
  1744. $selectColumns = $this->columns->getSelect($table, $columnNames);
  1745. $tableName = $table->getName();
  1746. $condition = $this->addMiddlewareConditions($tableName, $condition);
  1747. $parameters = array();
  1748. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  1749. $orderBy = $this->columns->getOrderBy($table, $columnOrdering);
  1750. $offsetLimit = $this->columns->getOffsetLimit($offset, $limit);
  1751. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '"' . $whereClause . ' ORDER BY ' . $orderBy . ' ' . $offsetLimit;
  1752. $stmt = $this->query($sql, $parameters);
  1753. $records = $stmt->fetchAll();
  1754. $this->converter->convertRecords($table, $columnNames, $records);
  1755. return $records;
  1756. }
  1757. public function updateSingle(ReflectedTable $table, array $columnValues, String $id)
  1758. {
  1759. if (count($columnValues) == 0) {
  1760. return 0;
  1761. }
  1762. $this->converter->convertColumnValues($table, $columnValues);
  1763. $updateColumns = $this->columns->getUpdate($table, $columnValues);
  1764. $tableName = $table->getName();
  1765. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  1766. $condition = $this->addMiddlewareConditions($tableName, $condition);
  1767. $parameters = array_values($columnValues);
  1768. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  1769. $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
  1770. $stmt = $this->query($sql, $parameters);
  1771. return $stmt->rowCount();
  1772. }
  1773. public function deleteSingle(ReflectedTable $table, String $id)
  1774. {
  1775. $tableName = $table->getName();
  1776. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  1777. $condition = $this->addMiddlewareConditions($tableName, $condition);
  1778. $parameters = array();
  1779. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  1780. $sql = 'DELETE FROM "' . $tableName . '" ' . $whereClause;
  1781. $stmt = $this->query($sql, $parameters);
  1782. return $stmt->rowCount();
  1783. }
  1784. public function incrementSingle(ReflectedTable $table, array $columnValues, String $id)
  1785. {
  1786. if (count($columnValues) == 0) {
  1787. return 0;
  1788. }
  1789. $this->converter->convertColumnValues($table, $columnValues);
  1790. $updateColumns = $this->columns->getIncrement($table, $columnValues);
  1791. $tableName = $table->getName();
  1792. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  1793. $condition = $this->addMiddlewareConditions($tableName, $condition);
  1794. $parameters = array_values($columnValues);
  1795. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  1796. $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
  1797. $stmt = $this->query($sql, $parameters);
  1798. return $stmt->rowCount();
  1799. }
  1800. private function query(String $sql, array $parameters): \PDOStatement
  1801. {
  1802. $stmt = $this->pdo->prepare($sql);
  1803. $stmt->execute($parameters);
  1804. return $stmt;
  1805. }
  1806. }
  1807. // file: src/Tqdev/PhpCrudApi/Database/GenericDefinition.php
  1808. class GenericDefinition
  1809. {
  1810. private $pdo;
  1811. private $driver;
  1812. private $database;
  1813. private $typeConverter;
  1814. private $reflection;
  1815. public function __construct(\PDO $pdo, String $driver, String $database)
  1816. {
  1817. $this->pdo = $pdo;
  1818. $this->driver = $driver;
  1819. $this->database = $database;
  1820. $this->typeConverter = new TypeConverter($driver);
  1821. $this->reflection = new GenericReflection($pdo, $driver, $database);
  1822. }
  1823. private function quote(String $identifier): String
  1824. {
  1825. return '"' . str_replace('"', '', $identifier) . '"';
  1826. }
  1827. public function getColumnType(ReflectedColumn $column, bool $update): String
  1828. {
  1829. if ($this->driver == 'pgsql' && !$update && $column->getPk() && $this->canAutoIncrement($column)) {
  1830. return 'serial';
  1831. }
  1832. $type = $this->typeConverter->fromJdbc($column->getType());
  1833. if ($column->hasPrecision() && $column->hasScale()) {
  1834. $size = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
  1835. } else if ($column->hasPrecision()) {
  1836. $size = '(' . $column->getPrecision() . ')';
  1837. } else if ($column->hasLength()) {
  1838. $size = '(' . $column->getLength() . ')';
  1839. } else {
  1840. $size = '';
  1841. }
  1842. $null = $this->getColumnNullType($column, $update);
  1843. $auto = $this->getColumnAutoIncrement($column, $update);
  1844. return $type . $size . $null . $auto;
  1845. }
  1846. private function getPrimaryKey(String $tableName): String
  1847. {
  1848. $pks = $this->reflection->getTablePrimaryKeys($tableName);
  1849. if (count($pks) == 1) {
  1850. return $pks[0];
  1851. }
  1852. return "";
  1853. }
  1854. private function canAutoIncrement(ReflectedColumn $column): bool
  1855. {
  1856. return in_array($column->getType(), ['integer', 'bigint']);
  1857. }
  1858. private function getColumnAutoIncrement(ReflectedColumn $column, bool $update): String
  1859. {
  1860. if (!$this->canAutoIncrement($column)) {
  1861. return '';
  1862. }
  1863. switch ($this->driver) {
  1864. case 'mysql':
  1865. return $column->getPk() ? ' AUTO_INCREMENT' : '';
  1866. case 'pgsql':
  1867. return '';
  1868. case 'sqlsrv':
  1869. return ($column->getPk() && !$update) ? ' IDENTITY(1,1)' : '';
  1870. }
  1871. }
  1872. private function getColumnNullType(ReflectedColumn $column, bool $update): String
  1873. {
  1874. if ($this->driver == 'pgsql' && $update) {
  1875. return '';
  1876. }
  1877. return $column->getNullable() ? ' NULL' : ' NOT NULL';
  1878. }
  1879. private function getTableRenameSQL(String $tableName, String $newTableName): String
  1880. {
  1881. $p1 = $this->quote($tableName);
  1882. $p2 = $this->quote($newTableName);
  1883. switch ($this->driver) {
  1884. case 'mysql':
  1885. return "RENAME TABLE $p1 TO $p2";
  1886. case 'pgsql':
  1887. return "ALTER TABLE $p1 RENAME TO $p2";
  1888. case 'sqlsrv':
  1889. return "EXEC sp_rename $p1, $p2";
  1890. }
  1891. }
  1892. private function getColumnRenameSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
  1893. {
  1894. $p1 = $this->quote($tableName);
  1895. $p2 = $this->quote($columnName);
  1896. $p3 = $this->quote($newColumn->getName());
  1897. switch ($this->driver) {
  1898. case 'mysql':
  1899. $p4 = $this->getColumnType($newColumn, true);
  1900. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  1901. case 'pgsql':
  1902. return "ALTER TABLE $p1 RENAME COLUMN $p2 TO $p3";
  1903. case 'sqlsrv':
  1904. $p4 = $this->quote($tableName . '.' . $columnName);
  1905. return "EXEC sp_rename $p4, $p3, 'COLUMN'";
  1906. }
  1907. }
  1908. private function getColumnRetypeSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
  1909. {
  1910. $p1 = $this->quote($tableName);
  1911. $p2 = $this->quote($columnName);
  1912. $p3 = $this->quote($newColumn->getName());
  1913. $p4 = $this->getColumnType($newColumn, true);
  1914. switch ($this->driver) {
  1915. case 'mysql':
  1916. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  1917. case 'pgsql':
  1918. return "ALTER TABLE $p1 ALTER COLUMN $p3 TYPE $p4";
  1919. case 'sqlsrv':
  1920. return "ALTER TABLE $p1 ALTER COLUMN $p3 $p4";
  1921. }
  1922. }
  1923. private function getSetColumnNullableSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
  1924. {
  1925. $p1 = $this->quote($tableName);
  1926. $p2 = $this->quote($columnName);
  1927. $p3 = $this->quote($newColumn->getName());
  1928. $p4 = $this->getColumnType($newColumn, true);
  1929. switch ($this->driver) {
  1930. case 'mysql':
  1931. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  1932. case 'pgsql':
  1933. $p5 = $newColumn->getNullable() ? 'DROP NOT NULL' : 'SET NOT NULL';
  1934. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p5";
  1935. case 'sqlsrv':
  1936. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
  1937. }
  1938. }
  1939. private function getSetColumnPkConstraintSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
  1940. {
  1941. $p1 = $this->quote($tableName);
  1942. $p2 = $this->quote($columnName);
  1943. $p3 = $this->quote($tableName . '_pkey');
  1944. switch ($this->driver) {
  1945. case 'mysql':
  1946. $p4 = $newColumn->getPk() ? "ADD PRIMARY KEY ($p2)" : 'DROP PRIMARY KEY';
  1947. return "ALTER TABLE $p1 $p4";
  1948. case 'pgsql':
  1949. case 'sqlsrv':
  1950. $p4 = $newColumn->getPk() ? "ADD PRIMARY KEY ($p2)" : "DROP CONSTRAINT $p3";
  1951. return "ALTER TABLE $p1 $p4";
  1952. }
  1953. }
  1954. private function getSetColumnPkSequenceSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
  1955. {
  1956. $p1 = $this->quote($tableName);
  1957. $p2 = $this->quote($columnName);
  1958. $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
  1959. switch ($this->driver) {
  1960. case 'mysql':
  1961. return "select 1";
  1962. case 'pgsql':
  1963. return $newColumn->getPk() ? "CREATE SEQUENCE $p3 OWNED BY $p1.$p2" : "DROP SEQUENCE $p3";
  1964. case 'sqlsrv':
  1965. return $newColumn->getPk() ? "CREATE SEQUENCE $p3" : "DROP SEQUENCE $p3";
  1966. }
  1967. }
  1968. private function getSetColumnPkSequenceStartSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
  1969. {
  1970. $p1 = $this->quote($tableName);
  1971. $p2 = $this->quote($columnName);
  1972. $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
  1973. switch ($this->driver) {
  1974. case 'mysql':
  1975. return "select 1";
  1976. case 'pgsql':
  1977. return "SELECT setval($p3, (SELECT max($p2)+1 FROM $p1));";
  1978. case 'sqlsrv':
  1979. return "ALTER SEQUENCE $p3 RESTART WITH (SELECT max($p2)+1 FROM $p1)";
  1980. }
  1981. }
  1982. private function getSetColumnPkDefaultSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
  1983. {
  1984. $p1 = $this->quote($tableName);
  1985. $p2 = $this->quote($columnName);
  1986. switch ($this->driver) {
  1987. case 'mysql':
  1988. $p3 = $this->quote($newColumn->getName());
  1989. $p4 = $this->getColumnType($newColumn, true);
  1990. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  1991. case 'pgsql':
  1992. if ($newColumn->getPk()) {
  1993. $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
  1994. $p4 = "SET DEFAULT nextval($p3)";
  1995. } else {
  1996. $p4 = 'DROP DEFAULT';
  1997. }
  1998. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
  1999. case 'sqlsrv':
  2000. $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
  2001. $p4 = $this->quote('DF_' . $tableName . '_' . $columnName);
  2002. if ($newColumn->getPk()) {
  2003. return "ALTER TABLE $p1 ADD CONSTRAINT $p4 DEFAULT NEXT VALUE FOR $p3 FOR $p2";
  2004. } else {
  2005. return "ALTER TABLE $p1 DROP CONSTRAINT $p4";
  2006. }
  2007. }
  2008. }
  2009. private function getAddColumnFkConstraintSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
  2010. {
  2011. $p1 = $this->quote($tableName);
  2012. $p2 = $this->quote($columnName);
  2013. $p3 = $this->quote($tableName . '_' . $columnName . '_fkey');
  2014. $p4 = $this->quote($newColumn->getFk());
  2015. $p5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
  2016. return "ALTER TABLE $p1 ADD CONSTRAINT $p3 FOREIGN KEY ($p2) REFERENCES $p4 ($p5)";
  2017. }
  2018. private function getRemoveColumnFkConstraintSQL(String $tableName, String $columnName, ReflectedColumn $newColumn): String
  2019. {
  2020. $p1 = $this->quote($tableName);
  2021. $p2 = $this->quote($tableName . '_' . $columnName . '_fkey');
  2022. switch ($this->driver) {
  2023. case 'mysql':
  2024. return "ALTER TABLE $p1 DROP FOREIGN KEY $p2";
  2025. case 'pgsql':
  2026. case 'sqlsrv':
  2027. return "ALTER TABLE $p1 DROP CONSTRAINT $p2";
  2028. }
  2029. }
  2030. private function getAddTableSQL(ReflectedTable $newTable): String
  2031. {
  2032. $tableName = $newTable->getName();
  2033. $p1 = $this->quote($tableName);
  2034. $fields = [];
  2035. $constraints = [];
  2036. foreach ($newTable->columnNames() as $columnName) {
  2037. $newColumn = $newTable->getColumn($columnName);
  2038. $f1 = $this->quote($columnName);
  2039. $f2 = $this->getColumnType($newColumn, false);
  2040. $f3 = $this->quote($tableName . '_' . $columnName . '_fkey');
  2041. $f4 = $this->quote($newColumn->getFk());
  2042. $f5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
  2043. $fields[] = "$f1 $f2";
  2044. if ($newColumn->getPk()) {
  2045. $constraints[] = "PRIMARY KEY ($f1)";
  2046. }
  2047. if ($newColumn->getFk()) {
  2048. $constraints[] = "CONSTRAINT $f3 FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
  2049. }
  2050. }
  2051. $p2 = implode(',', array_merge($fields, $constraints));
  2052. return "CREATE TABLE $p1 ($p2);";
  2053. }
  2054. private function getAddColumnSQL(String $tableName, ReflectedColumn $newColumn): String
  2055. {
  2056. $p1 = $this->quote($tableName);
  2057. $p2 = $this->quote($newColumn->getName());
  2058. $p3 = $this->getColumnType($newColumn, false);
  2059. return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
  2060. }
  2061. private function getRemoveTableSQL(String $tableName): String
  2062. {
  2063. $p1 = $this->quote($tableName);
  2064. return "DROP TABLE $p1 CASCADE;";
  2065. }
  2066. private function getRemoveColumnSQL(String $tableName, String $columnName): String
  2067. {
  2068. $p1 = $this->quote($tableName);
  2069. $p2 = $this->quote($columnName);
  2070. return "ALTER TABLE $p1 DROP COLUMN $p2 CASCADE;";
  2071. }
  2072. public function renameTable(String $tableName, String $newTableName)
  2073. {
  2074. $sql = $this->getTableRenameSQL($tableName, $newTableName);
  2075. return $this->query($sql);
  2076. }
  2077. public function renameColumn(String $tableName, String $columnName, ReflectedColumn $newColumn)
  2078. {
  2079. $sql = $this->getColumnRenameSQL($tableName, $columnName, $newColumn);
  2080. return $this->query($sql);
  2081. }
  2082. public function retypeColumn(String $tableName, String $columnName, ReflectedColumn $newColumn)
  2083. {
  2084. $sql = $this->getColumnRetypeSQL($tableName, $columnName, $newColumn);
  2085. return $this->query($sql);
  2086. }
  2087. public function setColumnNullable(String $tableName, String $columnName, ReflectedColumn $newColumn)
  2088. {
  2089. $sql = $this->getSetColumnNullableSQL($tableName, $columnName, $newColumn);
  2090. return $this->query($sql);
  2091. }
  2092. public function addColumnPrimaryKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
  2093. {
  2094. $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
  2095. $this->query($sql);
  2096. if ($this->canAutoIncrement($newColumn)) {
  2097. $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
  2098. $this->query($sql);
  2099. $sql = $this->getSetColumnPkSequenceStartSQL($tableName, $columnName, $newColumn);
  2100. $this->query($sql);
  2101. $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
  2102. $this->query($sql);
  2103. }
  2104. return true;
  2105. }
  2106. public function removeColumnPrimaryKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
  2107. {
  2108. if ($this->canAutoIncrement($newColumn)) {
  2109. $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
  2110. $this->query($sql);
  2111. $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
  2112. $this->query($sql);
  2113. }
  2114. $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
  2115. $this->query($sql);
  2116. return true;
  2117. }
  2118. public function addColumnForeignKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
  2119. {
  2120. $sql = $this->getAddColumnFkConstraintSQL($tableName, $columnName, $newColumn);
  2121. return $this->query($sql);
  2122. }
  2123. public function removeColumnForeignKey(String $tableName, String $columnName, ReflectedColumn $newColumn)
  2124. {
  2125. $sql = $this->getRemoveColumnFkConstraintSQL($tableName, $columnName, $newColumn);
  2126. return $this->query($sql);
  2127. }
  2128. public function addTable(ReflectedTable $newTable)
  2129. {
  2130. $sql = $this->getAddTableSQL($newTable);
  2131. return $this->query($sql);
  2132. }
  2133. public function addColumn(String $tableName, ReflectedColumn $newColumn)
  2134. {
  2135. $sql = $this->getAddColumnSQL($tableName, $newColumn);
  2136. return $this->query($sql);
  2137. }
  2138. public function removeTable(String $tableName)
  2139. {
  2140. $sql = $this->getRemoveTableSQL($tableName);
  2141. return $this->query($sql);
  2142. }
  2143. public function removeColumn(String $tableName, String $columnName)
  2144. {
  2145. $sql = $this->getRemoveColumnSQL($tableName, $columnName);
  2146. return $this->query($sql);
  2147. }
  2148. private function query(String $sql): bool
  2149. {
  2150. $stmt = $this->pdo->prepare($sql);
  2151. return $stmt->execute();
  2152. }
  2153. }
  2154. // file: src/Tqdev/PhpCrudApi/Database/GenericReflection.php
  2155. class GenericReflection
  2156. {
  2157. private $pdo;
  2158. private $driver;
  2159. private $database;
  2160. private $typeConverter;
  2161. public function __construct(\PDO $pdo, String $driver, String $database)
  2162. {
  2163. $this->pdo = $pdo;
  2164. $this->driver = $driver;
  2165. $this->database = $database;
  2166. $this->typeConverter = new TypeConverter($driver);
  2167. }
  2168. public function getIgnoredTables(): array
  2169. {
  2170. switch ($this->driver) {
  2171. case 'mysql':return [];
  2172. case 'pgsql':return ['spatial_ref_sys', 'raster_columns', 'raster_overviews', 'geography_columns', 'geometry_columns'];
  2173. case 'sqlsrv':return [];
  2174. }
  2175. }
  2176. private function getTablesSQL(): String
  2177. {
  2178. switch ($this->driver) {
  2179. case 'mysql':return 'SELECT "TABLE_NAME", "TABLE_TYPE" FROM "INFORMATION_SCHEMA"."TABLES" WHERE "TABLE_TYPE" IN (\'BASE TABLE\' , \'VIEW\') AND "TABLE_SCHEMA" = ? ORDER BY BINARY "TABLE_NAME"';
  2180. case 'pgsql':return 'SELECT c.relname as "TABLE_NAME", c.relkind as "TABLE_TYPE" FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN (\'r\', \'v\') AND n.nspname <> \'pg_catalog\' AND n.nspname <> \'information_schema\' AND n.nspname !~ \'^pg_toast\' AND pg_catalog.pg_table_is_visible(c.oid) AND \'\' <> ? ORDER BY "TABLE_NAME";';
  2181. case 'sqlsrv':return 'SELECT o.name as "TABLE_NAME", o.xtype as "TABLE_TYPE" FROM sysobjects o WHERE o.xtype IN (\'U\', \'V\') ORDER BY "TABLE_NAME"';
  2182. }
  2183. }
  2184. private function getTableColumnsSQL(): String
  2185. {
  2186. switch ($this->driver) {
  2187. case 'mysql':return 'SELECT "COLUMN_NAME", "IS_NULLABLE", "DATA_TYPE", "CHARACTER_MAXIMUM_LENGTH", "NUMERIC_PRECISION", "NUMERIC_SCALE" FROM "INFORMATION_SCHEMA"."COLUMNS" WHERE "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
  2188. case 'pgsql':return 'SELECT a.attname AS "COLUMN_NAME", case when a.attnotnull then \'NO\' else \'YES\' end as "IS_NULLABLE", pg_catalog.format_type(a.atttypid, -1) as "DATA_TYPE", case when a.atttypmod < 0 then NULL else a.atttypmod-4 end as "CHARACTER_MAXIMUM_LENGTH", case when a.atttypid != 1700 then NULL else ((a.atttypmod - 4) >> 16) & 65535 end as "NUMERIC_PRECISION", case when a.atttypid != 1700 then NULL else (a.atttypmod - 4) & 65535 end as "NUMERIC_SCALE" FROM pg_attribute a JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND a.attnum > 0 AND NOT a.attisdropped;';
  2189. case 'sqlsrv':return 'SELECT c.name AS "COLUMN_NAME", c.is_nullable AS "IS_NULLABLE", t.Name AS "DATA_TYPE", (c.max_length/2) AS "CHARACTER_MAXIMUM_LENGTH", c.precision AS "NUMERIC_PRECISION", c.scale AS "NUMERIC_SCALE" FROM sys.columns c INNER JOIN sys.types t ON c.user_type_id = t.user_type_id WHERE c.object_id = OBJECT_ID(?) AND \'\' <> ?';
  2190. }
  2191. }
  2192. private function getTablePrimaryKeysSQL(): String
  2193. {
  2194. switch ($this->driver) {
  2195. case 'mysql':return 'SELECT "COLUMN_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "CONSTRAINT_NAME" = \'PRIMARY\' AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
  2196. case 'pgsql':return 'SELECT a.attname AS "COLUMN_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'p\'';
  2197. case 'sqlsrv':return 'SELECT c.NAME as "COLUMN_NAME" FROM sys.key_constraints kc inner join sys.objects t on t.object_id = kc.parent_object_id INNER JOIN sys.index_columns ic ON kc.parent_object_id = ic.object_id and kc.unique_index_id = ic.index_id INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id WHERE kc.type = \'PK\' and t.object_id = OBJECT_ID(?) and \'\' <> ?';
  2198. }
  2199. }
  2200. private function getTableForeignKeysSQL(): String
  2201. {
  2202. switch ($this->driver) {
  2203. case 'mysql':return 'SELECT "COLUMN_NAME", "REFERENCED_TABLE_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "REFERENCED_TABLE_NAME" IS NOT NULL AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
  2204. case 'pgsql':return 'SELECT a.attname AS "COLUMN_NAME", c.confrelid::regclass::text AS "REFERENCED_TABLE_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'f\'';
  2205. case 'sqlsrv':return 'SELECT COL_NAME(fc.parent_object_id, fc.parent_column_id) AS "COLUMN_NAME", OBJECT_NAME (f.referenced_object_id) AS "REFERENCED_TABLE_NAME" FROM sys.foreign_keys AS f INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id WHERE f.parent_object_id = OBJECT_ID(?) and \'\' <> ?';
  2206. }
  2207. }
  2208. public function getDatabaseName(): String
  2209. {
  2210. return $this->database;
  2211. }
  2212. public function getTables(): array
  2213. {
  2214. $sql = $this->getTablesSQL();
  2215. $results = $this->query($sql, [$this->database]);
  2216. foreach ($results as &$result) {
  2217. switch ($this->driver) {
  2218. case 'mysql':
  2219. $map = ['BASE TABLE' => 'table', 'VIEW' => 'view'];
  2220. $result['TABLE_TYPE'] = $map[$result['TABLE_TYPE']];
  2221. break;
  2222. case 'pgsql':
  2223. $map = ['r' => 'table', 'v' => 'view'];
  2224. $result['TABLE_TYPE'] = $map[$result['TABLE_TYPE']];
  2225. break;
  2226. case 'sqlsrv':
  2227. $map = ['U' => 'table', 'V' => 'view'];
  2228. $result['TABLE_TYPE'] = $map[trim($result['TABLE_TYPE'])];
  2229. break;
  2230. }
  2231. }
  2232. return $results;
  2233. }
  2234. public function getTableColumns(String $tableName, String $type): array
  2235. {
  2236. $sql = $this->getTableColumnsSQL();
  2237. $results = $this->query($sql, [$tableName, $this->database]);
  2238. if ($type == 'view') {
  2239. foreach ($results as &$result) {
  2240. $result['IS_NULLABLE'] = false;
  2241. }
  2242. }
  2243. return $results;
  2244. }
  2245. public function getTablePrimaryKeys(String $tableName): array
  2246. {
  2247. $sql = $this->getTablePrimaryKeysSQL();
  2248. $results = $this->query($sql, [$tableName, $this->database]);
  2249. $primaryKeys = [];
  2250. foreach ($results as $result) {
  2251. $primaryKeys[] = $result['COLUMN_NAME'];
  2252. }
  2253. return $primaryKeys;
  2254. }
  2255. public function getTableForeignKeys(String $tableName): array
  2256. {
  2257. $sql = $this->getTableForeignKeysSQL();
  2258. $results = $this->query($sql, [$tableName, $this->database]);
  2259. $foreignKeys = [];
  2260. foreach ($results as $result) {
  2261. $foreignKeys[$result['COLUMN_NAME']] = $result['REFERENCED_TABLE_NAME'];
  2262. }
  2263. return $foreignKeys;
  2264. }
  2265. public function toJdbcType(String $type, int $size): String
  2266. {
  2267. return $this->typeConverter->toJdbc($type, $size);
  2268. }
  2269. private function query(String $sql, array $parameters): array
  2270. {
  2271. $stmt = $this->pdo->prepare($sql);
  2272. $stmt->execute($parameters);
  2273. return $stmt->fetchAll();
  2274. }
  2275. }
  2276. // file: src/Tqdev/PhpCrudApi/Database/TypeConverter.php
  2277. class TypeConverter
  2278. {
  2279. private $driver;
  2280. public function __construct(String $driver)
  2281. {
  2282. $this->driver = $driver;
  2283. }
  2284. private $fromJdbc = [
  2285. 'mysql' => [
  2286. 'clob' => 'longtext',
  2287. 'boolean' => 'bit',
  2288. 'blob' => 'longblob',
  2289. 'timestamp' => 'datetime',
  2290. ],
  2291. 'pgsql' => [
  2292. 'clob' => 'text',
  2293. 'blob' => 'bytea',
  2294. ],
  2295. 'sqlsrv' => [
  2296. 'boolean' => 'bit',
  2297. 'varchar' => 'nvarchar',
  2298. 'clob' => 'ntext',
  2299. 'blob' => 'image',
  2300. ],
  2301. ];
  2302. private $toJdbc = [
  2303. 'simplified' => [
  2304. 'char' => 'varchar',
  2305. 'longvarchar' => 'clob',
  2306. 'nchar' => 'varchar',
  2307. 'nvarchar' => 'varchar',
  2308. 'longnvarchar' => 'clob',
  2309. 'binary' => 'varbinary',
  2310. 'longvarbinary' => 'blob',
  2311. 'tinyint' => 'integer',
  2312. 'smallint' => 'integer',
  2313. 'real' => 'float',
  2314. 'numeric' => 'decimal',
  2315. 'time_with_timezone' => 'time',
  2316. 'timestamp_with_timezone' => 'timestamp',
  2317. ],
  2318. 'mysql' => [
  2319. 'tinyint(1)' => 'boolean',
  2320. 'bit(0)' => 'boolean',
  2321. 'bit(1)' => 'boolean',
  2322. 'tinyblob' => 'blob',
  2323. 'mediumblob' => 'blob',
  2324. 'longblob' => 'blob',
  2325. 'tinytext' => 'clob',
  2326. 'mediumtext' => 'clob',
  2327. 'longtext' => 'clob',
  2328. 'text' => 'clob',
  2329. 'mediumint' => 'integer',
  2330. 'int' => 'integer',
  2331. 'polygon' => 'geometry',
  2332. 'point' => 'geometry',
  2333. 'datetime' => 'timestamp',
  2334. 'enum' => 'varchar',
  2335. 'json' => 'clob',
  2336. ],
  2337. 'pgsql' => [
  2338. 'bigserial' => 'bigint',
  2339. 'bit varying' => 'bit',
  2340. 'box' => 'geometry',
  2341. 'bytea' => 'blob',
  2342. 'character varying' => 'varchar',
  2343. 'character' => 'char',
  2344. 'cidr' => 'varchar',
  2345. 'circle' => 'geometry',
  2346. 'double precision' => 'double',
  2347. 'inet' => 'integer',
  2348. 'json' => 'clob',
  2349. 'jsonb' => 'clob',
  2350. 'line' => 'geometry',
  2351. 'lseg' => 'geometry',
  2352. 'macaddr' => 'varchar',
  2353. 'money' => 'decimal',
  2354. 'path' => 'geometry',
  2355. 'point' => 'geometry',
  2356. 'polygon' => 'geometry',
  2357. 'real' => 'float',
  2358. 'serial' => 'integer',
  2359. 'text' => 'clob',
  2360. 'time without time zone' => 'time',
  2361. 'time with time zone' => 'time_with_timezone',
  2362. 'timestamp without time zone' => 'timestamp',
  2363. 'timestamp with time zone' => 'timestamp_with_timezone',
  2364. 'uuid' => 'char',
  2365. 'xml' => 'clob',
  2366. ],
  2367. 'sqlsrv' => [
  2368. 'varbinary(0)' => 'blob',
  2369. 'bit' => 'boolean',
  2370. 'datetime' => 'timestamp',
  2371. 'datetime2' => 'timestamp',
  2372. 'float' => 'double',
  2373. 'image' => 'blob',
  2374. 'int' => 'integer',
  2375. 'money' => 'decimal',
  2376. 'ntext' => 'clob',
  2377. 'smalldatetime' => 'timestamp',
  2378. 'smallmoney' => 'decimal',
  2379. 'text' => 'clob',
  2380. 'timestamp' => 'binary',
  2381. 'udt' => 'varbinary',
  2382. 'uniqueidentifier' => 'char',
  2383. 'xml' => 'clob',
  2384. ],
  2385. ];
  2386. private $valid = [
  2387. 'bigint' => true,
  2388. 'binary' => true,
  2389. 'bit' => true,
  2390. 'blob' => true,
  2391. 'boolean' => true,
  2392. 'char' => true,
  2393. 'clob' => true,
  2394. 'date' => true,
  2395. 'decimal' => true,
  2396. 'distinct' => true,
  2397. 'double' => true,
  2398. 'float' => true,
  2399. 'integer' => true,
  2400. 'longnvarchar' => true,
  2401. 'longvarbinary' => true,
  2402. 'longvarchar' => true,
  2403. 'nchar' => true,
  2404. 'nclob' => true,
  2405. 'numeric' => true,
  2406. 'nvarchar' => true,
  2407. 'real' => true,
  2408. 'smallint' => true,
  2409. 'time' => true,
  2410. 'time_with_timezone' => true,
  2411. 'timestamp' => true,
  2412. 'timestamp_with_timezone' => true,
  2413. 'tinyint' => true,
  2414. 'varbinary' => true,
  2415. 'varchar' => true,
  2416. 'geometry' => true,
  2417. ];
  2418. public function toJdbc(String $type, int $size): String
  2419. {
  2420. $jdbcType = strtolower($type);
  2421. if (isset($this->toJdbc[$this->driver]["$jdbcType($size)"])) {
  2422. $jdbcType = $this->toJdbc[$this->driver]["$jdbcType($size)"];
  2423. }
  2424. if (isset($this->toJdbc[$this->driver][$jdbcType])) {
  2425. $jdbcType = $this->toJdbc[$this->driver][$jdbcType];
  2426. }
  2427. if (isset($this->toJdbc['simplified'][$jdbcType])) {
  2428. $jdbcType = $this->toJdbc['simplified'][$jdbcType];
  2429. }
  2430. if (!isset($this->valid[$jdbcType])) {
  2431. throw new \Exception("Unsupported type '$jdbcType' for driver '$this->driver'");
  2432. }
  2433. return $jdbcType;
  2434. }
  2435. public function fromJdbc(String $type): String
  2436. {
  2437. $jdbcType = strtolower($type);
  2438. if (isset($this->fromJdbc[$this->driver][$jdbcType])) {
  2439. $jdbcType = $this->fromJdbc[$this->driver][$jdbcType];
  2440. }
  2441. return $jdbcType;
  2442. }
  2443. }
  2444. // file: src/Tqdev/PhpCrudApi/Middleware/Base/Handler.php
  2445. interface Handler
  2446. {
  2447. public function handle(Request $request): Response;
  2448. }
  2449. // file: src/Tqdev/PhpCrudApi/Middleware/Base/Middleware.php
  2450. abstract class Middleware implements Handler
  2451. {
  2452. protected $next;
  2453. protected $responder;
  2454. private $properties;
  2455. public function __construct(Router $router, Responder $responder, array $properties)
  2456. {
  2457. $router->load($this);
  2458. $this->responder = $responder;
  2459. $this->properties = $properties;
  2460. }
  2461. public function setNext(Handler $handler) /*: void*/
  2462. {
  2463. $this->next = $handler;
  2464. }
  2465. protected function getArrayProperty(String $key, String $default): array
  2466. {
  2467. return array_filter(array_map('trim', explode(',', $this->getProperty($key, $default))));
  2468. }
  2469. protected function getProperty(String $key, $default)
  2470. {
  2471. return isset($this->properties[$key]) ? $this->properties[$key] : $default;
  2472. }
  2473. }
  2474. // file: src/Tqdev/PhpCrudApi/Middleware/Communication/VariableStore.php
  2475. class VariableStore
  2476. {
  2477. static $values = array();
  2478. public static function get(String $key)
  2479. {
  2480. if (isset(self::$values[$key])) {
  2481. return self::$values[$key];
  2482. }
  2483. return null;
  2484. }
  2485. public static function set(String $key, /* object */ $value)
  2486. {
  2487. self::$values[$key] = $value;
  2488. }
  2489. }
  2490. // file: src/Tqdev/PhpCrudApi/Middleware/Router/Router.php
  2491. interface Router extends Handler
  2492. {
  2493. public function register(String $method, String $path, array $handler);
  2494. public function load(Middleware $middleware);
  2495. public function route(Request $request): Response;
  2496. }
  2497. // file: src/Tqdev/PhpCrudApi/Middleware/Router/SimpleRouter.php
  2498. class SimpleRouter implements Router
  2499. {
  2500. private $responder;
  2501. private $cache;
  2502. private $ttl;
  2503. private $registration;
  2504. private $routes;
  2505. private $routeHandlers;
  2506. private $middlewares;
  2507. public function __construct(Responder $responder, Cache $cache, int $ttl)
  2508. {
  2509. $this->responder = $responder;
  2510. $this->cache = $cache;
  2511. $this->ttl = $ttl;
  2512. $this->registration = true;
  2513. $this->routes = $this->loadPathTree();
  2514. $this->routeHandlers = [];
  2515. $this->middlewares = array();
  2516. }
  2517. private function loadPathTree(): PathTree
  2518. {
  2519. $data = $this->cache->get('PathTree');
  2520. if ($data != '') {
  2521. $tree = PathTree::fromJson(json_decode(gzuncompress($data)));
  2522. $this->registration = false;
  2523. } else {
  2524. $tree = new PathTree();
  2525. }
  2526. return $tree;
  2527. }
  2528. public function register(String $method, String $path, array $handler)
  2529. {
  2530. $routeNumber = count($this->routeHandlers);
  2531. $this->routeHandlers[$routeNumber] = $handler;
  2532. if ($this->registration) {
  2533. $parts = explode('/', trim($path, '/'));
  2534. array_unshift($parts, $method);
  2535. $this->routes->put($parts, $routeNumber);
  2536. }
  2537. }
  2538. public function load(Middleware $middleware) /*: void*/
  2539. {
  2540. if (count($this->middlewares) > 0) {
  2541. $next = $this->middlewares[0];
  2542. } else {
  2543. $next = $this;
  2544. }
  2545. $middleware->setNext($next);
  2546. array_unshift($this->middlewares, $middleware);
  2547. }
  2548. public function route(Request $request): Response
  2549. {
  2550. if ($this->registration) {
  2551. $data = gzcompress(json_encode($this->routes, JSON_UNESCAPED_UNICODE));
  2552. $this->cache->set('PathTree', $data, $this->ttl);
  2553. }
  2554. $obj = $this;
  2555. if (count($this->middlewares) > 0) {
  2556. $obj = $this->middlewares[0];
  2557. }
  2558. return $obj->handle($request);
  2559. }
  2560. private function getRouteNumbers(Request $request): array
  2561. {
  2562. $method = strtoupper($request->getMethod());
  2563. $path = explode('/', trim($request->getPath(0), '/'));
  2564. array_unshift($path, $method);
  2565. return $this->routes->match($path);
  2566. }
  2567. public function handle(Request $request): Response
  2568. {
  2569. $routeNumbers = $this->getRouteNumbers($request);
  2570. if (count($routeNumbers) == 0) {
  2571. return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getPath());
  2572. }
  2573. return call_user_func($this->routeHandlers[$routeNumbers[0]], $request);
  2574. }
  2575. }
  2576. // file: src/Tqdev/PhpCrudApi/Middleware/AjaxOnlyMiddleware.php
  2577. class AjaxOnlyMiddleware extends Middleware
  2578. {
  2579. public function handle(Request $request): Response
  2580. {
  2581. $method = $request->getMethod();
  2582. $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
  2583. if (!in_array($method, $excludeMethods)) {
  2584. $headerName = $this->getProperty('headerName', 'X-Requested-With');
  2585. $headerValue = $this->getProperty('headerValue', 'XMLHttpRequest');
  2586. if ($headerValue != $request->getHeader($headerName)) {
  2587. return $this->responder->error(ErrorCode::ONLY_AJAX_REQUESTS_ALLOWED, $method);
  2588. }
  2589. }
  2590. return $this->next->handle($request);
  2591. }
  2592. }
  2593. // file: src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php
  2594. class AuthorizationMiddleware extends Middleware
  2595. {
  2596. private $reflection;
  2597. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  2598. {
  2599. parent::__construct($router, $responder, $properties);
  2600. $this->reflection = $reflection;
  2601. $this->utils = new RequestUtils($reflection);
  2602. }
  2603. private function handleColumns(String $operation, String $tableName) /*: void*/
  2604. {
  2605. $columnHandler = $this->getProperty('columnHandler', '');
  2606. if ($columnHandler) {
  2607. $table = $this->reflection->getTable($tableName);
  2608. foreach ($table->columnNames() as $columnName) {
  2609. $allowed = call_user_func($columnHandler, $operation, $tableName, $columnName);
  2610. if (!$allowed) {
  2611. $table->removeColumn($columnName);
  2612. }
  2613. }
  2614. }
  2615. }
  2616. private function handleTable(String $operation, String $tableName) /*: void*/
  2617. {
  2618. if (!$this->reflection->hasTable($tableName)) {
  2619. return;
  2620. }
  2621. $tableHandler = $this->getProperty('tableHandler', '');
  2622. if ($tableHandler) {
  2623. $allowed = call_user_func($tableHandler, $operation, $tableName);
  2624. if (!$allowed) {
  2625. $this->reflection->removeTable($tableName);
  2626. } else {
  2627. $this->handleColumns($operation, $tableName);
  2628. }
  2629. }
  2630. }
  2631. private function handleRecords(String $operation, String $tableName) /*: void*/
  2632. {
  2633. if (!$this->reflection->hasTable($tableName)) {
  2634. return;
  2635. }
  2636. $recordHandler = $this->getProperty('recordHandler', '');
  2637. if ($recordHandler) {
  2638. $query = call_user_func($recordHandler, $operation, $tableName);
  2639. $filters = new FilterInfo();
  2640. $table = $this->reflection->getTable($tableName);
  2641. $query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
  2642. parse_str($query, $params);
  2643. $condition = $filters->getCombinedConditions($table, $params);
  2644. VariableStore::set("authorization.conditions.$tableName", $condition);
  2645. }
  2646. }
  2647. public function handle(Request $request): Response
  2648. {
  2649. $path = $request->getPathSegment(1);
  2650. $operation = $this->utils->getOperation($request);
  2651. $tableNames = $this->utils->getTableNames($request);
  2652. foreach ($tableNames as $tableName) {
  2653. $this->handleTable($operation, $tableName);
  2654. if ($path == 'records') {
  2655. $this->handleRecords($operation, $tableName);
  2656. }
  2657. }
  2658. if ($path == 'openapi') {
  2659. VariableStore::set('authorization.tableHandler', $this->getProperty('tableHandler', ''));
  2660. VariableStore::set('authorization.columnHandler', $this->getProperty('columnHandler', ''));
  2661. }
  2662. return $this->next->handle($request);
  2663. }
  2664. }
  2665. // file: src/Tqdev/PhpCrudApi/Middleware/BasicAuthMiddleware.php
  2666. class BasicAuthMiddleware extends Middleware
  2667. {
  2668. private function hasCorrectPassword(String $username, String $password, array &$passwords): bool
  2669. {
  2670. $hash = isset($passwords[$username]) ? $passwords[$username] : false;
  2671. if ($hash && password_verify($password, $hash)) {
  2672. if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
  2673. $passwords[$username] = password_hash($password, PASSWORD_DEFAULT);
  2674. }
  2675. return true;
  2676. }
  2677. return false;
  2678. }
  2679. private function getValidUsername(String $username, String $password, String $passwordFile): String
  2680. {
  2681. $passwords = $this->readPasswords($passwordFile);
  2682. $valid = $this->hasCorrectPassword($username, $password, $passwords);
  2683. $this->writePasswords($passwordFile, $passwords);
  2684. return $valid ? $username : '';
  2685. }
  2686. private function readPasswords(String $passwordFile): array
  2687. {
  2688. $passwords = [];
  2689. $passwordLines = file($passwordFile);
  2690. foreach ($passwordLines as $passwordLine) {
  2691. if (strpos($passwordLine, ':') !== false) {
  2692. list($username, $hash) = explode(':', trim($passwordLine), 2);
  2693. if (strlen($hash) > 0 && $hash[0] != '$') {
  2694. $hash = password_hash($hash, PASSWORD_DEFAULT);
  2695. }
  2696. $passwords[$username] = $hash;
  2697. }
  2698. }
  2699. return $passwords;
  2700. }
  2701. private function writePasswords(String $passwordFile, array $passwords): bool
  2702. {
  2703. $success = false;
  2704. $passwordFileContents = '';
  2705. foreach ($passwords as $username => $hash) {
  2706. $passwordFileContents .= "$username:$hash\n";
  2707. }
  2708. if (file_get_contents($passwordFile) != $passwordFileContents) {
  2709. $success = file_put_contents($passwordFile, $passwordFileContents) !== false;
  2710. }
  2711. return $success;
  2712. }
  2713. private function getAuthorizationCredentials(Request $request): String
  2714. {
  2715. if (isset($_SERVER['PHP_AUTH_USER'])) {
  2716. return $_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW'];
  2717. }
  2718. $parts = explode(' ', trim($request->getHeader('Authorization')), 2);
  2719. if (count($parts) != 2) {
  2720. return '';
  2721. }
  2722. if ($parts[0] != 'Basic') {
  2723. return '';
  2724. }
  2725. return base64_decode(strtr($parts[1], '-_', '+/'));
  2726. }
  2727. public function handle(Request $request): Response
  2728. {
  2729. if (session_status() == PHP_SESSION_NONE) {
  2730. session_start();
  2731. }
  2732. $credentials = $this->getAuthorizationCredentials($request);
  2733. if ($credentials) {
  2734. list($username, $password) = array('', '');
  2735. if (strpos($credentials, ':') !== false) {
  2736. list($username, $password) = explode(':', $credentials, 2);
  2737. }
  2738. $passwordFile = $this->getProperty('passwordFile', '.htpasswd');
  2739. $validUser = $this->getValidUsername($username, $password, $passwordFile);
  2740. $_SESSION['username'] = $validUser;
  2741. if (!$validUser) {
  2742. return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
  2743. }
  2744. if (!headers_sent()) {
  2745. session_regenerate_id();
  2746. }
  2747. }
  2748. if (!isset($_SESSION['username']) || !$_SESSION['username']) {
  2749. $authenticationMode = $this->getProperty('mode', 'required');
  2750. if ($authenticationMode == 'required') {
  2751. $response = $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
  2752. $realm = $this->getProperty('realm', 'Username and password required');
  2753. $response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
  2754. return $response;
  2755. }
  2756. }
  2757. return $this->next->handle($request);
  2758. }
  2759. }
  2760. // file: src/Tqdev/PhpCrudApi/Middleware/CorsMiddleware.php
  2761. class CorsMiddleware extends Middleware
  2762. {
  2763. private function isOriginAllowed(String $origin, String $allowedOrigins): bool
  2764. {
  2765. $found = false;
  2766. foreach (explode(',', $allowedOrigins) as $allowedOrigin) {
  2767. $hostname = preg_quote(strtolower(trim($allowedOrigin)));
  2768. $regex = '/^' . str_replace('\*', '.*', $hostname) . '$/';
  2769. if (preg_match($regex, $origin)) {
  2770. $found = true;
  2771. break;
  2772. }
  2773. }
  2774. return $found;
  2775. }
  2776. public function handle(Request $request): Response
  2777. {
  2778. $method = $request->getMethod();
  2779. $origin = $request->getHeader('Origin');
  2780. $allowedOrigins = $this->getProperty('allowedOrigins', '*');
  2781. if ($origin && !$this->isOriginAllowed($origin, $allowedOrigins)) {
  2782. $response = $this->responder->error(ErrorCode::ORIGIN_FORBIDDEN, $origin);
  2783. } elseif ($method == 'OPTIONS') {
  2784. $response = new Response(Response::OK, '');
  2785. $allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization');
  2786. if ($allowHeaders) {
  2787. $response->addHeader('Access-Control-Allow-Headers', $allowHeaders);
  2788. }
  2789. $allowMethods = $this->getProperty('allowMethods', 'OPTIONS, GET, PUT, POST, DELETE, PATCH');
  2790. if ($allowMethods) {
  2791. $response->addHeader('Access-Control-Allow-Methods', $allowMethods);
  2792. }
  2793. $allowCredentials = $this->getProperty('allowCredentials', 'true');
  2794. if ($allowCredentials) {
  2795. $response->addHeader('Access-Control-Allow-Credentials', $allowCredentials);
  2796. }
  2797. $maxAge = $this->getProperty('maxAge', '1728000');
  2798. if ($maxAge) {
  2799. $response->addHeader('Access-Control-Max-Age', $maxAge);
  2800. }
  2801. $exposeHeaders = $this->getProperty('exposeHeaders', '');
  2802. if ($exposeHeaders) {
  2803. $response->addHeader('Access-Control-Expose-Headers', $exposeHeaders);
  2804. }
  2805. } else {
  2806. $response = $this->next->handle($request);
  2807. }
  2808. if ($origin) {
  2809. $allowCredentials = $this->getProperty('allowCredentials', 'true');
  2810. if ($allowCredentials) {
  2811. $response->addHeader('Access-Control-Allow-Credentials', $allowCredentials);
  2812. }
  2813. $response->addHeader('Access-Control-Allow-Origin', $origin);
  2814. }
  2815. return $response;
  2816. }
  2817. }
  2818. // file: src/Tqdev/PhpCrudApi/Middleware/CustomizationMiddleware.php
  2819. class CustomizationMiddleware extends Middleware
  2820. {
  2821. private $reflection;
  2822. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  2823. {
  2824. parent::__construct($router, $responder, $properties);
  2825. $this->reflection = $reflection;
  2826. $this->utils = new RequestUtils($reflection);
  2827. }
  2828. public function handle(Request $request): Response
  2829. {
  2830. $operation = $this->utils->getOperation($request);
  2831. $tableName = $request->getPathSegment(2);
  2832. $beforeHandler = $this->getProperty('beforeHandler', '');
  2833. $environment = (object) array();
  2834. if ($beforeHandler !== '') {
  2835. call_user_func($beforeHandler, $operation, $tableName, $request, $environment);
  2836. }
  2837. $response = $this->next->handle($request);
  2838. $afterHandler = $this->getProperty('afterHandler', '');
  2839. if ($afterHandler !== '') {
  2840. call_user_func($afterHandler, $operation, $tableName, $response, $environment);
  2841. }
  2842. return $response;
  2843. }
  2844. }
  2845. // file: src/Tqdev/PhpCrudApi/Middleware/FileUploadMiddleware.php
  2846. class FileUploadMiddleware extends Middleware
  2847. {
  2848. public function handle(Request $request): Response
  2849. {
  2850. $files = $request->getUploadedFiles();
  2851. if (!empty($files)) {
  2852. $body = $request->getBody();
  2853. foreach ($files as $fieldName => $file) {
  2854. if (isset($file['error']) && $file['error']) {
  2855. return $this->responder->error(ErrorCode::FILE_UPLOAD_FAILED, $fieldName);
  2856. }
  2857. foreach ($file as $key => $value) {
  2858. if ($key == 'tmp_name') {
  2859. $value = base64_encode(file_get_contents($value));
  2860. $key = $fieldName;
  2861. } else {
  2862. $key = $fieldName . '_' . $key;
  2863. }
  2864. $body->$key = $value;
  2865. }
  2866. }
  2867. $request->setBody($body);
  2868. }
  2869. return $this->next->handle($request);
  2870. }
  2871. }
  2872. // file: src/Tqdev/PhpCrudApi/Middleware/FirewallMiddleware.php
  2873. class FirewallMiddleware extends Middleware
  2874. {
  2875. private function ipMatch(String $ip, String $cidr): bool
  2876. {
  2877. if (strpos($cidr, '/') !== false) {
  2878. list($subnet, $mask) = explode('/', trim($cidr));
  2879. if ((ip2long($ip) & ~((1 << (32 - $mask)) - 1)) == ip2long($subnet)) {
  2880. return true;
  2881. }
  2882. } else {
  2883. if (ip2long($ip) == ip2long($cidr)) {
  2884. return true;
  2885. }
  2886. }
  2887. return false;
  2888. }
  2889. private function isIpAllowed(String $ipAddress, String $allowedIpAddresses): bool
  2890. {
  2891. foreach (explode(',', $allowedIpAddresses) as $allowedIp) {
  2892. if ($this->ipMatch($ipAddress, $allowedIp)) {
  2893. return true;
  2894. }
  2895. }
  2896. return false;
  2897. }
  2898. public function handle(Request $request): Response
  2899. {
  2900. $reverseProxy = $this->getProperty('reverseProxy', '');
  2901. if ($reverseProxy) {
  2902. $ipAddress = array_pop(explode(',', $request->getHeader('X-Forwarded-For')));
  2903. } elseif (isset($_SERVER['REMOTE_ADDR'])) {
  2904. $ipAddress = $_SERVER['REMOTE_ADDR'];
  2905. } else {
  2906. $ipAddress = '127.0.0.1';
  2907. }
  2908. $allowedIpAddresses = $this->getProperty('allowedIpAddresses', '');
  2909. if (!$this->isIpAllowed($ipAddress, $allowedIpAddresses)) {
  2910. $response = $this->responder->error(ErrorCode::TEMPORARY_OR_PERMANENTLY_BLOCKED, '');
  2911. } else {
  2912. $response = $this->next->handle($request);
  2913. }
  2914. return $response;
  2915. }
  2916. }
  2917. // file: src/Tqdev/PhpCrudApi/Middleware/JwtAuthMiddleware.php
  2918. class JwtAuthMiddleware extends Middleware
  2919. {
  2920. private function getVerifiedClaims(String $token, int $time, int $leeway, int $ttl, String $secret, array $requirements): array
  2921. {
  2922. $algorithms = array('HS256' => 'sha256', 'HS384' => 'sha384', 'HS512' => 'sha512');
  2923. $token = explode('.', $token);
  2924. if (count($token) < 3) {
  2925. return array();
  2926. }
  2927. $header = json_decode(base64_decode(strtr($token[0], '-_', '+/')), true);
  2928. if (!$secret) {
  2929. return array();
  2930. }
  2931. if ($header['typ'] != 'JWT') {
  2932. return array();
  2933. }
  2934. $algorithm = $header['alg'];
  2935. if (!isset($algorithms[$algorithm])) {
  2936. return array();
  2937. }
  2938. $hmac = $algorithms[$algorithm];
  2939. $signature = bin2hex(base64_decode(strtr($token[2], '-_', '+/')));
  2940. if ($signature != hash_hmac($hmac, "$token[0].$token[1]", $secret)) {
  2941. return array();
  2942. }
  2943. $claims = json_decode(base64_decode(strtr($token[1], '-_', '+/')), true);
  2944. if (!$claims) {
  2945. return array();
  2946. }
  2947. foreach ($requirements as $field => $values) {
  2948. if (!empty($values)) {
  2949. if ($field == 'alg') {
  2950. if (!isset($header[$field]) || !in_array($header[$field], $values)) {
  2951. return array();
  2952. }
  2953. } else {
  2954. if (!isset($claims[$field]) || !in_array($claims[$field], $values)) {
  2955. return array();
  2956. }
  2957. }
  2958. }
  2959. }
  2960. if (isset($claims['nbf']) && $time + $leeway < $claims['nbf']) {
  2961. return array();
  2962. }
  2963. if (isset($claims['iat']) && $time + $leeway < $claims['iat']) {
  2964. return array();
  2965. }
  2966. if (isset($claims['exp']) && $time - $leeway > $claims['exp']) {
  2967. return array();
  2968. }
  2969. if (isset($claims['iat']) && !isset($claims['exp'])) {
  2970. if ($time - $leeway > $claims['iat'] + $ttl) {
  2971. return array();
  2972. }
  2973. }
  2974. return $claims;
  2975. }
  2976. private function getClaims(String $token): array
  2977. {
  2978. $time = (int) $this->getProperty('time', time());
  2979. $leeway = (int) $this->getProperty('leeway', '5');
  2980. $ttl = (int) $this->getProperty('ttl', '30');
  2981. $secret = $this->getProperty('secret', '');
  2982. $requirements = array(
  2983. 'alg' => $this->getArrayProperty('algorithms', ''),
  2984. 'aud' => $this->getArrayProperty('audiences', ''),
  2985. 'iss' => $this->getArrayProperty('issuers', ''),
  2986. );
  2987. if (!$secret) {
  2988. return array();
  2989. }
  2990. return $this->getVerifiedClaims($token, $time, $leeway, $ttl, $secret, $requirements);
  2991. }
  2992. private function getAuthorizationToken(Request $request): String
  2993. {
  2994. $header = $this->getProperty('header', 'X-Authorization');
  2995. $parts = explode(' ', trim($request->getHeader($header)), 2);
  2996. if (count($parts) != 2) {
  2997. return '';
  2998. }
  2999. if ($parts[0] != 'Bearer') {
  3000. return '';
  3001. }
  3002. return $parts[1];
  3003. }
  3004. public function handle(Request $request): Response
  3005. {
  3006. if (session_status() == PHP_SESSION_NONE) {
  3007. session_start();
  3008. }
  3009. $token = $this->getAuthorizationToken($request);
  3010. if ($token) {
  3011. $claims = $this->getClaims($token);
  3012. $_SESSION['claims'] = $claims;
  3013. if (empty($claims)) {
  3014. return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, 'JWT');
  3015. }
  3016. if (!headers_sent()) {
  3017. session_regenerate_id();
  3018. }
  3019. }
  3020. if (empty($_SESSION['claims'])) {
  3021. $authenticationMode = $this->getProperty('mode', 'required');
  3022. if ($authenticationMode == 'required') {
  3023. return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
  3024. }
  3025. }
  3026. return $this->next->handle($request);
  3027. }
  3028. }
  3029. // file: src/Tqdev/PhpCrudApi/Middleware/MultiTenancyMiddleware.php
  3030. class MultiTenancyMiddleware extends Middleware
  3031. {
  3032. private $reflection;
  3033. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  3034. {
  3035. parent::__construct($router, $responder, $properties);
  3036. $this->reflection = $reflection;
  3037. $this->utils = new RequestUtils($reflection);
  3038. }
  3039. private function getCondition(String $tableName, array $pairs): Condition
  3040. {
  3041. $condition = new NoCondition();
  3042. $table = $this->reflection->getTable($tableName);
  3043. foreach ($pairs as $k => $v) {
  3044. $condition = $condition->_and(new ColumnCondition($table->getColumn($k), 'eq', $v));
  3045. }
  3046. return $condition;
  3047. }
  3048. private function getPairs($handler, String $operation, String $tableName): array
  3049. {
  3050. $result = array();
  3051. $pairs = call_user_func($handler, $operation, $tableName);
  3052. $table = $this->reflection->getTable($tableName);
  3053. foreach ($pairs as $k => $v) {
  3054. if ($table->hasColumn($k)) {
  3055. $result[$k] = $v;
  3056. }
  3057. }
  3058. return $result;
  3059. }
  3060. private function handleRecord(Request $request, String $operation, array $pairs) /*: void*/
  3061. {
  3062. $record = $request->getBody();
  3063. if ($record === null) {
  3064. return;
  3065. }
  3066. $multi = is_array($record);
  3067. $records = $multi ? $record : [$record];
  3068. foreach ($records as &$record) {
  3069. foreach ($pairs as $column => $value) {
  3070. if ($operation == 'create') {
  3071. $record->$column = $value;
  3072. } else {
  3073. if (isset($record->$column)) {
  3074. unset($record->$column);
  3075. }
  3076. }
  3077. }
  3078. }
  3079. $request->setBody($multi ? $records : $records[0]);
  3080. }
  3081. public function handle(Request $request): Response
  3082. {
  3083. $handler = $this->getProperty('handler', '');
  3084. if ($handler !== '') {
  3085. $path = $request->getPathSegment(1);
  3086. if ($path == 'records') {
  3087. $operation = $this->utils->getOperation($request);
  3088. $tableNames = $this->utils->getTableNames($request);
  3089. foreach ($tableNames as $i => $tableName) {
  3090. if (!$this->reflection->hasTable($tableName)) {
  3091. continue;
  3092. }
  3093. $pairs = $this->getPairs($handler, $operation, $tableName);
  3094. if ($i == 0) {
  3095. if (in_array($operation, ['create', 'update', 'increment'])) {
  3096. $this->handleRecord($request, $operation, $pairs);
  3097. }
  3098. }
  3099. $condition = $this->getCondition($tableName, $pairs);
  3100. VariableStore::set("multiTenancy.conditions.$tableName", $condition);
  3101. }
  3102. }
  3103. }
  3104. return $this->next->handle($request);
  3105. }
  3106. }
  3107. // file: src/Tqdev/PhpCrudApi/Middleware/SanitationMiddleware.php
  3108. class SanitationMiddleware extends Middleware
  3109. {
  3110. private $reflection;
  3111. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  3112. {
  3113. parent::__construct($router, $responder, $properties);
  3114. $this->reflection = $reflection;
  3115. $this->utils = new RequestUtils($reflection);
  3116. }
  3117. private function callHandler($handler, $record, String $operation, ReflectedTable $table) /*: object */
  3118. {
  3119. $context = (array) $record;
  3120. $tableName = $table->getName();
  3121. foreach ($context as $columnName => &$value) {
  3122. if ($table->hasColumn($columnName)) {
  3123. $column = $table->getColumn($columnName);
  3124. $value = call_user_func($handler, $operation, $tableName, $column->serialize(), $value);
  3125. }
  3126. }
  3127. return (object) $context;
  3128. }
  3129. public function handle(Request $request): Response
  3130. {
  3131. $operation = $this->utils->getOperation($request);
  3132. if (in_array($operation, ['create', 'update', 'increment'])) {
  3133. $tableName = $request->getPathSegment(2);
  3134. if ($this->reflection->hasTable($tableName)) {
  3135. $record = $request->getBody();
  3136. if ($record !== null) {
  3137. $handler = $this->getProperty('handler', '');
  3138. if ($handler !== '') {
  3139. $table = $this->reflection->getTable($tableName);
  3140. if (is_array($record)) {
  3141. foreach ($record as &$r) {
  3142. $r = $this->callHandler($handler, $r, $operation, $table);
  3143. }
  3144. } else {
  3145. $record = $this->callHandler($handler, $record, $operation, $table);
  3146. }
  3147. $request->setBody($record);
  3148. }
  3149. }
  3150. }
  3151. }
  3152. return $this->next->handle($request);
  3153. }
  3154. }
  3155. // file: src/Tqdev/PhpCrudApi/Middleware/ValidationMiddleware.php
  3156. class ValidationMiddleware extends Middleware
  3157. {
  3158. private $reflection;
  3159. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  3160. {
  3161. parent::__construct($router, $responder, $properties);
  3162. $this->reflection = $reflection;
  3163. $this->utils = new RequestUtils($reflection);
  3164. }
  3165. private function callHandler($handler, $record, String $operation, ReflectedTable $table) /*: Response?*/
  3166. {
  3167. $context = (array) $record;
  3168. $details = array();
  3169. $tableName = $table->getName();
  3170. foreach ($context as $columnName => $value) {
  3171. if ($table->hasColumn($columnName)) {
  3172. $column = $table->getColumn($columnName);
  3173. $valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
  3174. if ($valid !== true && $valid !== '') {
  3175. $details[$columnName] = $valid;
  3176. }
  3177. }
  3178. }
  3179. if (count($details) > 0) {
  3180. return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
  3181. }
  3182. return null;
  3183. }
  3184. public function handle(Request $request): Response
  3185. {
  3186. $operation = $this->utils->getOperation($request);
  3187. if (in_array($operation, ['create', 'update', 'increment'])) {
  3188. $tableName = $request->getPathSegment(2);
  3189. if ($this->reflection->hasTable($tableName)) {
  3190. $record = $request->getBody();
  3191. if ($record !== null) {
  3192. $handler = $this->getProperty('handler', '');
  3193. if ($handler !== '') {
  3194. $table = $this->reflection->getTable($tableName);
  3195. if (is_array($record)) {
  3196. foreach ($record as $r) {
  3197. $response = $this->callHandler($handler, $r, $operation, $table);
  3198. if ($response !== null) {
  3199. return $response;
  3200. }
  3201. }
  3202. } else {
  3203. $response = $this->callHandler($handler, $record, $operation, $table);
  3204. if ($response !== null) {
  3205. return $response;
  3206. }
  3207. }
  3208. }
  3209. }
  3210. }
  3211. }
  3212. return $this->next->handle($request);
  3213. }
  3214. }
  3215. // file: src/Tqdev/PhpCrudApi/Middleware/XsrfMiddleware.php
  3216. class XsrfMiddleware extends Middleware
  3217. {
  3218. private function getToken(): String
  3219. {
  3220. $cookieName = $this->getProperty('cookieName', 'XSRF-TOKEN');
  3221. if (isset($_COOKIE[$cookieName])) {
  3222. $token = $_COOKIE[$cookieName];
  3223. } else {
  3224. $secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
  3225. $token = bin2hex(random_bytes(8));
  3226. if (!headers_sent()) {
  3227. setcookie($cookieName, $token, 0, '', '', $secure);
  3228. }
  3229. }
  3230. return $token;
  3231. }
  3232. public function handle(Request $request): Response
  3233. {
  3234. $token = $this->getToken();
  3235. $method = $request->getMethod();
  3236. $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
  3237. if (!in_array($method, $excludeMethods)) {
  3238. $headerName = $this->getProperty('headerName', 'X-XSRF-TOKEN');
  3239. if ($token != $request->getHeader($headerName)) {
  3240. return $this->responder->error(ErrorCode::BAD_OR_MISSING_XSRF_TOKEN, '');
  3241. }
  3242. }
  3243. return $this->next->handle($request);
  3244. }
  3245. }
  3246. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiBuilder.php
  3247. class OpenApiBuilder
  3248. {
  3249. private $openapi;
  3250. private $reflection;
  3251. private $operations = [
  3252. 'list' => 'get',
  3253. 'create' => 'post',
  3254. 'read' => 'get',
  3255. 'update' => 'put',
  3256. 'delete' => 'delete',
  3257. 'increment' => 'patch',
  3258. ];
  3259. private $types = [
  3260. 'integer' => ['type' => 'integer', 'format' => 'int32'],
  3261. 'bigint' => ['type' => 'integer', 'format' => 'int64'],
  3262. 'varchar' => ['type' => 'string'],
  3263. 'clob' => ['type' => 'string'],
  3264. 'varbinary' => ['type' => 'string', 'format' => 'byte'],
  3265. 'blob' => ['type' => 'string', 'format' => 'byte'],
  3266. 'decimal' => ['type' => 'string'],
  3267. 'float' => ['type' => 'number', 'format' => 'float'],
  3268. 'double' => ['type' => 'number', 'format' => 'double'],
  3269. 'time' => ['type' => 'string', 'format' => 'date-time'],
  3270. 'timestamp' => ['type' => 'string', 'format' => 'date-time'],
  3271. 'geometry' => ['type' => 'string'],
  3272. 'boolean' => ['type' => 'boolean'],
  3273. ];
  3274. public function __construct(ReflectionService $reflection, $base)
  3275. {
  3276. $this->reflection = $reflection;
  3277. $this->openapi = new OpenApiDefinition($base);
  3278. }
  3279. public function build(): OpenApiDefinition
  3280. {
  3281. $this->openapi->set("openapi", "3.0.0");
  3282. $tableNames = $this->reflection->getTableNames();
  3283. foreach ($tableNames as $tableName) {
  3284. $this->setPath($tableName);
  3285. }
  3286. $this->openapi->set("components|responses|pk_integer|description", "inserted primary key value (integer)");
  3287. $this->openapi->set("components|responses|pk_integer|content|application/json|schema|type", "integer");
  3288. $this->openapi->set("components|responses|pk_integer|content|application/json|schema|format", "int64");
  3289. $this->openapi->set("components|responses|pk_string|description", "inserted primary key value (string)");
  3290. $this->openapi->set("components|responses|pk_string|content|application/json|schema|type", "string");
  3291. $this->openapi->set("components|responses|pk_string|content|application/json|schema|format", "uuid");
  3292. $this->openapi->set("components|responses|rows_affected|description", "number of rows affected (integer)");
  3293. $this->openapi->set("components|responses|rows_affected|content|application/json|schema|type", "integer");
  3294. $this->openapi->set("components|responses|rows_affected|content|application/json|schema|format", "int64");
  3295. foreach ($tableNames as $tableName) {
  3296. $this->setComponentSchema($tableName);
  3297. $this->setComponentResponse($tableName);
  3298. $this->setComponentRequestBody($tableName);
  3299. }
  3300. $this->setComponentParameters();
  3301. foreach ($tableNames as $index => $tableName) {
  3302. $this->setTag($index, $tableName);
  3303. }
  3304. return $this->openapi;
  3305. }
  3306. private function isOperationOnTableAllowed(String $operation, String $tableName): bool
  3307. {
  3308. $tableHandler = VariableStore::get('authorization.tableHandler');
  3309. if (!$tableHandler) {
  3310. return true;
  3311. }
  3312. return (bool) call_user_func($tableHandler, $operation, $tableName);
  3313. }
  3314. private function isOperationOnColumnAllowed(String $operation, String $tableName, String $columnName): bool
  3315. {
  3316. $columnHandler = VariableStore::get('authorization.columnHandler');
  3317. if (!$columnHandler) {
  3318. return true;
  3319. }
  3320. return (bool) call_user_func($columnHandler, $operation, $tableName, $columnName);
  3321. }
  3322. private function setPath(String $tableName) /*: void*/
  3323. {
  3324. $table = $this->reflection->getTable($tableName);
  3325. $type = $table->getType($tableName);
  3326. $pk = $table->getPk();
  3327. $pkName = $pk ? $pk->getName() : '';
  3328. foreach ($this->operations as $operation => $method) {
  3329. if (!$pkName && $operation != 'list') {
  3330. continue;
  3331. }
  3332. if ($type != 'table' && $operation != 'list') {
  3333. continue;
  3334. }
  3335. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  3336. continue;
  3337. }
  3338. if (in_array($operation, ['list', 'create'])) {
  3339. $path = sprintf('/records/%s', $tableName);
  3340. } else {
  3341. $path = sprintf('/records/%s/{%s}', $tableName, $pkName);
  3342. $this->openapi->set("paths|$path|$method|parameters|0|\$ref", "#/components/parameters/pk");
  3343. }
  3344. if (in_array($operation, ['create', 'update', 'increment'])) {
  3345. $this->openapi->set("paths|$path|$method|requestBody|\$ref", "#/components/requestBodies/$operation-" . urlencode($tableName));
  3346. }
  3347. $this->openapi->set("paths|$path|$method|tags|0", "$tableName");
  3348. $this->openapi->set("paths|$path|$method|description", "$operation $tableName");
  3349. switch ($operation) {
  3350. case 'list':
  3351. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . urlencode($tableName));
  3352. break;
  3353. case 'create':
  3354. if ($pk->getType() == 'integer') {
  3355. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_integer");
  3356. } else {
  3357. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_string");
  3358. }
  3359. break;
  3360. case 'read':
  3361. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . urlencode($tableName));
  3362. break;
  3363. case 'update':
  3364. case 'delete':
  3365. case 'increment':
  3366. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/rows_affected");
  3367. break;
  3368. }
  3369. }
  3370. }
  3371. private function setComponentSchema(String $tableName) /*: void*/
  3372. {
  3373. $table = $this->reflection->getTable($tableName);
  3374. $type = $table->getType($tableName);
  3375. $pk = $table->getPk();
  3376. $pkName = $pk ? $pk->getName() : '';
  3377. foreach ($this->operations as $operation => $method) {
  3378. if (!$pkName && $operation != 'list') {
  3379. continue;
  3380. }
  3381. if ($type != 'table' && $operation != 'list') {
  3382. continue;
  3383. }
  3384. if ($operation == 'delete') {
  3385. continue;
  3386. }
  3387. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  3388. continue;
  3389. }
  3390. if ($operation == 'list') {
  3391. $this->openapi->set("components|schemas|$operation-$tableName|type", "object");
  3392. $this->openapi->set("components|schemas|$operation-$tableName|properties|results|type", "integer");
  3393. $this->openapi->set("components|schemas|$operation-$tableName|properties|results|format", "int64");
  3394. $this->openapi->set("components|schemas|$operation-$tableName|properties|records|type", "array");
  3395. $prefix = "components|schemas|$operation-$tableName|properties|records|items";
  3396. } else {
  3397. $prefix = "components|schemas|$operation-$tableName";
  3398. }
  3399. $this->openapi->set("$prefix|type", "object");
  3400. foreach ($table->columnNames() as $columnName) {
  3401. if (!$this->isOperationOnColumnAllowed($operation, $tableName, $columnName)) {
  3402. continue;
  3403. }
  3404. $column = $table->getColumn($columnName);
  3405. $properties = $this->types[$column->getType()];
  3406. foreach ($properties as $key => $value) {
  3407. $this->openapi->set("$prefix|properties|$columnName|$key", $value);
  3408. }
  3409. }
  3410. }
  3411. }
  3412. private function setComponentResponse(String $tableName) /*: void*/
  3413. {
  3414. $table = $this->reflection->getTable($tableName);
  3415. $type = $table->getType($tableName);
  3416. $pk = $table->getPk();
  3417. $pkName = $pk ? $pk->getName() : '';
  3418. foreach (['list', 'read'] as $operation) {
  3419. if (!$pkName && $operation != 'list') {
  3420. continue;
  3421. }
  3422. if ($type != 'table' && $operation != 'list') {
  3423. continue;
  3424. }
  3425. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  3426. continue;
  3427. }
  3428. if ($operation == 'list') {
  3429. $this->openapi->set("components|responses|$operation-$tableName|description", "list of $tableName records");
  3430. } else {
  3431. $this->openapi->set("components|responses|$operation-$tableName|description", "single $tableName record");
  3432. }
  3433. $this->openapi->set("components|responses|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . urlencode($tableName));
  3434. }
  3435. }
  3436. private function setComponentRequestBody(String $tableName) /*: void*/
  3437. {
  3438. $table = $this->reflection->getTable($tableName);
  3439. $type = $table->getType($tableName);
  3440. $pk = $table->getPk();
  3441. $pkName = $pk ? $pk->getName() : '';
  3442. if ($pkName && $type == 'table') {
  3443. foreach (['create', 'update', 'increment'] as $operation) {
  3444. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  3445. continue;
  3446. }
  3447. $this->openapi->set("components|requestBodies|$operation-$tableName|description", "single $tableName record");
  3448. $this->openapi->set("components|requestBodies|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . urlencode($tableName));
  3449. }
  3450. }
  3451. }
  3452. private function setComponentParameters() /*: void*/
  3453. {
  3454. $this->openapi->set("components|parameters|pk|name", "id");
  3455. $this->openapi->set("components|parameters|pk|in", "path");
  3456. $this->openapi->set("components|parameters|pk|schema|type", "string");
  3457. $this->openapi->set("components|parameters|pk|description", "primary key value");
  3458. $this->openapi->set("components|parameters|pk|required", true);
  3459. }
  3460. private function setTag(int $index, String $tableName) /*: void*/
  3461. {
  3462. $this->openapi->set("tags|$index|name", "$tableName");
  3463. $this->openapi->set("tags|$index|description", "$tableName operations");
  3464. }
  3465. }
  3466. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiDefinition.php
  3467. class OpenApiDefinition implements \JsonSerializable
  3468. {
  3469. private $root;
  3470. public function __construct($base)
  3471. {
  3472. $this->root = $base;
  3473. }
  3474. public function set(String $path, $value) /*: void*/
  3475. {
  3476. $parts = explode('|', trim($path, '|'));
  3477. $current = &$this->root;
  3478. while (count($parts) > 0) {
  3479. $part = array_shift($parts);
  3480. if (!isset($current[$part])) {
  3481. $current[$part] = [];
  3482. }
  3483. $current = &$current[$part];
  3484. }
  3485. $current = $value;
  3486. }
  3487. public function jsonSerialize()
  3488. {
  3489. return $this->root;
  3490. }
  3491. }
  3492. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiService.php
  3493. class OpenApiService
  3494. {
  3495. private $builder;
  3496. public function __construct(ReflectionService $reflection, array $base)
  3497. {
  3498. $this->builder = new OpenApiBuilder($reflection, $base);
  3499. }
  3500. public function get(): OpenApiDefinition
  3501. {
  3502. return $this->builder->build();
  3503. }
  3504. }
  3505. // file: src/Tqdev/PhpCrudApi/Record/Condition/AndCondition.php
  3506. class AndCondition extends Condition
  3507. {
  3508. private $conditions;
  3509. public function __construct(Condition $condition1, Condition $condition2)
  3510. {
  3511. $this->conditions = [$condition1, $condition2];
  3512. }
  3513. public function _and(Condition $condition): Condition
  3514. {
  3515. if ($condition instanceof NoCondition) {
  3516. return $this;
  3517. }
  3518. $this->conditions[] = $condition;
  3519. return $this;
  3520. }
  3521. public function getConditions(): array
  3522. {
  3523. return $this->conditions;
  3524. }
  3525. public static function fromArray(array $conditions): Condition
  3526. {
  3527. $condition = new NoCondition();
  3528. foreach ($conditions as $c) {
  3529. $condition = $condition->_and($c);
  3530. }
  3531. return $condition;
  3532. }
  3533. }
  3534. // file: src/Tqdev/PhpCrudApi/Record/Condition/ColumnCondition.php
  3535. class ColumnCondition extends Condition
  3536. {
  3537. private $column;
  3538. private $operator;
  3539. private $value;
  3540. public function __construct(ReflectedColumn $column, String $operator, String $value)
  3541. {
  3542. $this->column = $column;
  3543. $this->operator = $operator;
  3544. $this->value = $value;
  3545. }
  3546. public function getColumn(): ReflectedColumn
  3547. {
  3548. return $this->column;
  3549. }
  3550. public function getOperator(): String
  3551. {
  3552. return $this->operator;
  3553. }
  3554. public function getValue(): String
  3555. {
  3556. return $this->value;
  3557. }
  3558. }
  3559. // file: src/Tqdev/PhpCrudApi/Record/Condition/Condition.php
  3560. abstract class Condition
  3561. {
  3562. public function _and(Condition $condition): Condition
  3563. {
  3564. if ($condition instanceof NoCondition) {
  3565. return $this;
  3566. }
  3567. return new AndCondition($this, $condition);
  3568. }
  3569. public function _or(Condition $condition): Condition
  3570. {
  3571. if ($condition instanceof NoCondition) {
  3572. return $this;
  3573. }
  3574. return new OrCondition($this, $condition);
  3575. }
  3576. public function _not(): Condition
  3577. {
  3578. return new NotCondition($this);
  3579. }
  3580. public static function fromString(ReflectedTable $table, String $value): Condition
  3581. {
  3582. $condition = new NoCondition();
  3583. $parts = explode(',', $value, 3);
  3584. if (count($parts) < 2) {
  3585. return null;
  3586. }
  3587. $field = $table->getColumn($parts[0]);
  3588. $command = $parts[1];
  3589. $negate = false;
  3590. $spatial = false;
  3591. if (strlen($command) > 2) {
  3592. if (substr($command, 0, 1) == 'n') {
  3593. $negate = true;
  3594. $command = substr($command, 1);
  3595. }
  3596. if (substr($command, 0, 1) == 's') {
  3597. $spatial = true;
  3598. $command = substr($command, 1);
  3599. }
  3600. }
  3601. if (count($parts) == 3 || (count($parts) == 2 && in_array($command, ['ic', 'is', 'iv']))) {
  3602. if ($spatial) {
  3603. if (in_array($command, ['co', 'cr', 'di', 'eq', 'in', 'ov', 'to', 'wi', 'ic', 'is', 'iv'])) {
  3604. $condition = new SpatialCondition($field, $command, $parts[2]);
  3605. }
  3606. } else {
  3607. if (in_array($command, ['cs', 'sw', 'ew', 'eq', 'lt', 'le', 'ge', 'gt', 'bt', 'in', 'is'])) {
  3608. $condition = new ColumnCondition($field, $command, $parts[2]);
  3609. }
  3610. }
  3611. }
  3612. if ($negate) {
  3613. $condition = $condition->_not();
  3614. }
  3615. return $condition;
  3616. }
  3617. }
  3618. // file: src/Tqdev/PhpCrudApi/Record/Condition/NoCondition.php
  3619. class NoCondition extends Condition
  3620. {
  3621. public function _and(Condition $condition): Condition
  3622. {
  3623. return $condition;
  3624. }
  3625. public function _or(Condition $condition): Condition
  3626. {
  3627. return $condition;
  3628. }
  3629. public function _not(): Condition
  3630. {
  3631. return $this;
  3632. }
  3633. }
  3634. // file: src/Tqdev/PhpCrudApi/Record/Condition/NotCondition.php
  3635. class NotCondition extends Condition
  3636. {
  3637. private $condition;
  3638. public function __construct(Condition $condition)
  3639. {
  3640. $this->condition = $condition;
  3641. }
  3642. public function getCondition(): Condition
  3643. {
  3644. return $this->condition;
  3645. }
  3646. }
  3647. // file: src/Tqdev/PhpCrudApi/Record/Condition/OrCondition.php
  3648. class OrCondition extends Condition
  3649. {
  3650. private $conditions;
  3651. public function __construct(Condition $condition1, Condition $condition2)
  3652. {
  3653. $this->conditions = [$condition1, $condition2];
  3654. }
  3655. public function _or(Condition $condition): Condition
  3656. {
  3657. if ($condition instanceof NoCondition) {
  3658. return $this;
  3659. }
  3660. $this->conditions[] = $condition;
  3661. return $this;
  3662. }
  3663. public function getConditions(): array
  3664. {
  3665. return $this->conditions;
  3666. }
  3667. public static function fromArray(array $conditions): Condition
  3668. {
  3669. $condition = new NoCondition();
  3670. foreach ($conditions as $c) {
  3671. $condition = $condition->_or($c);
  3672. }
  3673. return $condition;
  3674. }
  3675. }
  3676. // file: src/Tqdev/PhpCrudApi/Record/Condition/SpatialCondition.php
  3677. class SpatialCondition extends ColumnCondition
  3678. {
  3679. }
  3680. // file: src/Tqdev/PhpCrudApi/Record/Document/ErrorDocument.php
  3681. class ErrorDocument implements \JsonSerializable
  3682. {
  3683. public $code;
  3684. public $message;
  3685. public $details;
  3686. public function __construct(ErrorCode $errorCode, String $argument, $details)
  3687. {
  3688. $this->code = $errorCode->getCode();
  3689. $this->message = $errorCode->getMessage($argument);
  3690. $this->details = $details;
  3691. }
  3692. public function getCode(): int
  3693. {
  3694. return $this->code;
  3695. }
  3696. public function getMessage(): String
  3697. {
  3698. return $this->message;
  3699. }
  3700. public function serialize()
  3701. {
  3702. return [
  3703. 'code' => $this->code,
  3704. 'message' => $this->message,
  3705. 'details' => $this->details,
  3706. ];
  3707. }
  3708. public function jsonSerialize()
  3709. {
  3710. return array_filter($this->serialize());
  3711. }
  3712. }
  3713. // file: src/Tqdev/PhpCrudApi/Record/Document/ListDocument.php
  3714. class ListDocument implements \JsonSerializable
  3715. {
  3716. private $records;
  3717. private $results;
  3718. public function __construct(array $records, int $results)
  3719. {
  3720. $this->records = $records;
  3721. $this->results = $results;
  3722. }
  3723. public function getRecords(): array
  3724. {
  3725. return $this->records;
  3726. }
  3727. public function getResults(): int
  3728. {
  3729. return $this->results;
  3730. }
  3731. public function serialize()
  3732. {
  3733. return [
  3734. 'records' => $this->records,
  3735. 'results' => $this->results,
  3736. ];
  3737. }
  3738. public function jsonSerialize()
  3739. {
  3740. return array_filter($this->serialize(), function ($v) {
  3741. return $v !== 0;
  3742. });
  3743. }
  3744. }
  3745. // file: src/Tqdev/PhpCrudApi/Record/ColumnIncluder.php
  3746. class ColumnIncluder
  3747. {
  3748. private function isMandatory(String $tableName, String $columnName, array $params): bool
  3749. {
  3750. return isset($params['mandatory']) && in_array($tableName . "." . $columnName, $params['mandatory']);
  3751. }
  3752. private function select(String $tableName, bool $primaryTable, array $params, String $paramName,
  3753. array $columnNames, bool $include): array{
  3754. if (!isset($params[$paramName])) {
  3755. return $columnNames;
  3756. }
  3757. $columns = array();
  3758. foreach (explode(',', $params[$paramName][0]) as $columnName) {
  3759. $columns[$columnName] = true;
  3760. }
  3761. $result = array();
  3762. foreach ($columnNames as $columnName) {
  3763. $match = isset($columns['*.*']);
  3764. if (!$match) {
  3765. $match = isset($columns[$tableName . '.*']) || isset($columns[$tableName . '.' . $columnName]);
  3766. }
  3767. if ($primaryTable && !$match) {
  3768. $match = isset($columns['*']) || isset($columns[$columnName]);
  3769. }
  3770. if ($match) {
  3771. if ($include || $this->isMandatory($tableName, $columnName, $params)) {
  3772. $result[] = $columnName;
  3773. }
  3774. } else {
  3775. if (!$include || $this->isMandatory($tableName, $columnName, $params)) {
  3776. $result[] = $columnName;
  3777. }
  3778. }
  3779. }
  3780. return $result;
  3781. }
  3782. public function getNames(ReflectedTable $table, bool $primaryTable, array $params): array
  3783. {
  3784. $tableName = $table->getName();
  3785. $results = $table->columnNames();
  3786. $results = $this->select($tableName, $primaryTable, $params, 'include', $results, true);
  3787. $results = $this->select($tableName, $primaryTable, $params, 'exclude', $results, false);
  3788. return $results;
  3789. }
  3790. public function getValues(ReflectedTable $table, bool $primaryTable, /* object */ $record, array $params): array
  3791. {
  3792. $results = array();
  3793. $columnNames = $this->getNames($table, $primaryTable, $params);
  3794. foreach ($columnNames as $columnName) {
  3795. if (property_exists($record, $columnName)) {
  3796. $results[$columnName] = $record->$columnName;
  3797. }
  3798. }
  3799. return $results;
  3800. }
  3801. }
  3802. // file: src/Tqdev/PhpCrudApi/Record/ErrorCode.php
  3803. class ErrorCode
  3804. {
  3805. private $code;
  3806. private $message;
  3807. private $status;
  3808. const ERROR_NOT_FOUND = 9999;
  3809. const ROUTE_NOT_FOUND = 1000;
  3810. const TABLE_NOT_FOUND = 1001;
  3811. const ARGUMENT_COUNT_MISMATCH = 1002;
  3812. const RECORD_NOT_FOUND = 1003;
  3813. const ORIGIN_FORBIDDEN = 1004;
  3814. const COLUMN_NOT_FOUND = 1005;
  3815. const TABLE_ALREADY_EXISTS = 1006;
  3816. const COLUMN_ALREADY_EXISTS = 1007;
  3817. const HTTP_MESSAGE_NOT_READABLE = 1008;
  3818. const DUPLICATE_KEY_EXCEPTION = 1009;
  3819. const DATA_INTEGRITY_VIOLATION = 1010;
  3820. const AUTHENTICATION_REQUIRED = 1011;
  3821. const AUTHENTICATION_FAILED = 1012;
  3822. const INPUT_VALIDATION_FAILED = 1013;
  3823. const OPERATION_FORBIDDEN = 1014;
  3824. const OPERATION_NOT_SUPPORTED = 1015;
  3825. const TEMPORARY_OR_PERMANENTLY_BLOCKED = 1016;
  3826. const BAD_OR_MISSING_XSRF_TOKEN = 1017;
  3827. const ONLY_AJAX_REQUESTS_ALLOWED = 1018;
  3828. const FILE_UPLOAD_FAILED = 1019;
  3829. private $values = [
  3830. 9999 => ["%s", Response::INTERNAL_SERVER_ERROR],
  3831. 1000 => ["Route '%s' not found", Response::NOT_FOUND],
  3832. 1001 => ["Table '%s' not found", Response::NOT_FOUND],
  3833. 1002 => ["Argument count mismatch in '%s'", Response::UNPROCESSABLE_ENTITY],
  3834. 1003 => ["Record '%s' not found", Response::NOT_FOUND],
  3835. 1004 => ["Origin '%s' is forbidden", Response::FORBIDDEN],
  3836. 1005 => ["Column '%s' not found", Response::NOT_FOUND],
  3837. 1006 => ["Table '%s' already exists", Response::CONFLICT],
  3838. 1007 => ["Column '%s' already exists", Response::CONFLICT],
  3839. 1008 => ["Cannot read HTTP message", Response::UNPROCESSABLE_ENTITY],
  3840. 1009 => ["Duplicate key exception", Response::CONFLICT],
  3841. 1010 => ["Data integrity violation", Response::CONFLICT],
  3842. 1011 => ["Authentication required", Response::UNAUTHORIZED],
  3843. 1012 => ["Authentication failed for '%s'", Response::FORBIDDEN],
  3844. 1013 => ["Input validation failed for '%s'", Response::UNPROCESSABLE_ENTITY],
  3845. 1014 => ["Operation forbidden", Response::FORBIDDEN],
  3846. 1015 => ["Operation '%s' not supported", Response::METHOD_NOT_ALLOWED],
  3847. 1016 => ["Temporary or permanently blocked", Response::FORBIDDEN],
  3848. 1017 => ["Bad or missing XSRF token", Response::FORBIDDEN],
  3849. 1018 => ["Only AJAX requests allowed for '%s'", Response::FORBIDDEN],
  3850. 1019 => ["File upload failed for '%s'", Response::UNPROCESSABLE_ENTITY],
  3851. ];
  3852. public function __construct(int $code)
  3853. {
  3854. if (!isset($this->values[$code])) {
  3855. $code = 9999;
  3856. }
  3857. $this->code = $code;
  3858. $this->message = $this->values[$code][0];
  3859. $this->status = $this->values[$code][1];
  3860. }
  3861. public function getCode(): int
  3862. {
  3863. return $this->code;
  3864. }
  3865. public function getMessage(String $argument): String
  3866. {
  3867. return sprintf($this->message, $argument);
  3868. }
  3869. public function getStatus(): int
  3870. {
  3871. return $this->status;
  3872. }
  3873. }
  3874. // file: src/Tqdev/PhpCrudApi/Record/FilterInfo.php
  3875. class FilterInfo
  3876. {
  3877. private function addConditionFromFilterPath(PathTree $conditions, array $path, ReflectedTable $table, array $params)
  3878. {
  3879. $key = 'filter' . implode('', $path);
  3880. if (isset($params[$key])) {
  3881. foreach ($params[$key] as $filter) {
  3882. $condition = Condition::fromString($table, $filter);
  3883. if ($condition != null) {
  3884. $conditions->put($path, $condition);
  3885. }
  3886. }
  3887. }
  3888. }
  3889. private function getConditionsAsPathTree(ReflectedTable $table, array $params): PathTree
  3890. {
  3891. $conditions = new PathTree();
  3892. $this->addConditionFromFilterPath($conditions, [], $table, $params);
  3893. for ($n = ord('0'); $n <= ord('9'); $n++) {
  3894. $this->addConditionFromFilterPath($conditions, [chr($n)], $table, $params);
  3895. for ($l = ord('a'); $l <= ord('f'); $l++) {
  3896. $this->addConditionFromFilterPath($conditions, [chr($n), chr($l)], $table, $params);
  3897. }
  3898. }
  3899. return $conditions;
  3900. }
  3901. private function combinePathTreeOfConditions(PathTree $tree): Condition
  3902. {
  3903. $andConditions = $tree->getValues();
  3904. $and = AndCondition::fromArray($andConditions);
  3905. $orConditions = [];
  3906. foreach ($tree->getKeys() as $p) {
  3907. $orConditions[] = $this->combinePathTreeOfConditions($tree->get($p));
  3908. }
  3909. $or = OrCondition::fromArray($orConditions);
  3910. return $and->_and($or);
  3911. }
  3912. public function getCombinedConditions(ReflectedTable $table, array $params): Condition
  3913. {
  3914. return $this->combinePathTreeOfConditions($this->getConditionsAsPathTree($table, $params));
  3915. }
  3916. }
  3917. // file: src/Tqdev/PhpCrudApi/Record/HabtmValues.php
  3918. class HabtmValues
  3919. {
  3920. public $pkValues;
  3921. public $fkValues;
  3922. public function __construct(array $pkValues, array $fkValues)
  3923. {
  3924. $this->pkValues = $pkValues;
  3925. $this->fkValues = $fkValues;
  3926. }
  3927. }
  3928. // file: src/Tqdev/PhpCrudApi/Record/OrderingInfo.php
  3929. class OrderingInfo
  3930. {
  3931. public function getColumnOrdering(ReflectedTable $table, array $params): array
  3932. {
  3933. $fields = array();
  3934. if (isset($params['order'])) {
  3935. foreach ($params['order'] as $order) {
  3936. $parts = explode(',', $order, 3);
  3937. $columnName = $parts[0];
  3938. if (!$table->hasColumn($columnName)) {
  3939. continue;
  3940. }
  3941. $ascending = 'ASC';
  3942. if (count($parts) > 1) {
  3943. if (substr(strtoupper($parts[1]), 0, 4) == "DESC") {
  3944. $ascending = 'DESC';
  3945. }
  3946. }
  3947. $fields[] = [$columnName, $ascending];
  3948. }
  3949. }
  3950. if (count($fields) == 0) {
  3951. $pk = $table->getPk();
  3952. if ($pk) {
  3953. $fields[] = [$pk->getName(), 'ASC'];
  3954. } else {
  3955. foreach ($table->columnNames() as $columnName) {
  3956. $fields[] = [$columnName, 'ASC'];
  3957. }
  3958. }
  3959. }
  3960. return $fields;
  3961. }
  3962. }
  3963. // file: src/Tqdev/PhpCrudApi/Record/PaginationInfo.php
  3964. class PaginationInfo
  3965. {
  3966. public $DEFAULT_PAGE_SIZE = 20;
  3967. public function hasPage(array $params): bool
  3968. {
  3969. return isset($params['page']);
  3970. }
  3971. public function getPageOffset(array $params): int
  3972. {
  3973. $offset = 0;
  3974. $pageSize = $this->getPageSize($params);
  3975. if (isset($params['page'])) {
  3976. foreach ($params['page'] as $page) {
  3977. $parts = explode(',', $page, 2);
  3978. $page = intval($parts[0]) - 1;
  3979. $offset = $page * $pageSize;
  3980. }
  3981. }
  3982. return $offset;
  3983. }
  3984. public function getPageSize(array $params): int
  3985. {
  3986. $pageSize = $this->DEFAULT_PAGE_SIZE;
  3987. if (isset($params['page'])) {
  3988. foreach ($params['page'] as $page) {
  3989. $parts = explode(',', $page, 2);
  3990. if (count($parts) > 1) {
  3991. $pageSize = intval($parts[1]);
  3992. }
  3993. }
  3994. }
  3995. return $pageSize;
  3996. }
  3997. public function getResultSize(array $params): int
  3998. {
  3999. $numberOfRows = -1;
  4000. if (isset($params['size'])) {
  4001. foreach ($params['size'] as $size) {
  4002. $numberOfRows = intval($size);
  4003. }
  4004. }
  4005. return $numberOfRows;
  4006. }
  4007. }
  4008. // file: src/Tqdev/PhpCrudApi/Record/PathTree.php
  4009. class PathTree implements \JsonSerializable
  4010. {
  4011. const WILDCARD = '*';
  4012. private $tree;
  4013. public function __construct( /* object */&$tree = null)
  4014. {
  4015. if (!$tree) {
  4016. $tree = $this->newTree();
  4017. }
  4018. $this->tree = &$tree;
  4019. }
  4020. public function newTree()
  4021. {
  4022. return (object) ['values' => [], 'branches' => (object) []];
  4023. }
  4024. public function getKeys(): array
  4025. {
  4026. $branches = (array) $this->tree->branches;
  4027. return array_keys($branches);
  4028. }
  4029. public function getValues(): array
  4030. {
  4031. return $this->tree->values;
  4032. }
  4033. public function get(String $key): PathTree
  4034. {
  4035. if (!isset($this->tree->branches->$key)) {
  4036. return null;
  4037. }
  4038. return new PathTree($this->tree->branches->$key);
  4039. }
  4040. public function put(array $path, $value)
  4041. {
  4042. $tree = &$this->tree;
  4043. foreach ($path as $key) {
  4044. if (!isset($tree->branches->$key)) {
  4045. $tree->branches->$key = $this->newTree();
  4046. }
  4047. $tree = &$tree->branches->$key;
  4048. }
  4049. $tree->values[] = $value;
  4050. }
  4051. public function match(array $path): array
  4052. {
  4053. $star = self::WILDCARD;
  4054. $tree = &$this->tree;
  4055. foreach ($path as $key) {
  4056. if (isset($tree->branches->$key)) {
  4057. $tree = &$tree->branches->$key;
  4058. } else if (isset($tree->branches->$star)) {
  4059. $tree = &$tree->branches->$star;
  4060. } else {
  4061. return [];
  4062. }
  4063. }
  4064. return $tree->values;
  4065. }
  4066. public static function fromJson( /* object */$tree): PathTree
  4067. {
  4068. return new PathTree($tree);
  4069. }
  4070. public function jsonSerialize()
  4071. {
  4072. return $this->tree;
  4073. }
  4074. }
  4075. // file: src/Tqdev/PhpCrudApi/Record/RecordService.php
  4076. class RecordService
  4077. {
  4078. private $db;
  4079. private $reflection;
  4080. private $columns;
  4081. private $joiner;
  4082. private $filters;
  4083. private $ordering;
  4084. private $pagination;
  4085. public function __construct(GenericDB $db, ReflectionService $reflection)
  4086. {
  4087. $this->db = $db;
  4088. $this->reflection = $reflection;
  4089. $this->columns = new ColumnIncluder();
  4090. $this->joiner = new RelationJoiner($reflection, $this->columns);
  4091. $this->filters = new FilterInfo();
  4092. $this->ordering = new OrderingInfo();
  4093. $this->pagination = new PaginationInfo();
  4094. }
  4095. private function sanitizeRecord(String $tableName, /* object */ $record, String $id)
  4096. {
  4097. $keyset = array_keys((array) $record);
  4098. foreach ($keyset as $key) {
  4099. if (!$this->reflection->getTable($tableName)->hasColumn($key)) {
  4100. unset($record->$key);
  4101. }
  4102. }
  4103. if ($id != '') {
  4104. $pk = $this->reflection->getTable($tableName)->getPk();
  4105. foreach ($this->reflection->getTable($tableName)->columnNames() as $key) {
  4106. $field = $this->reflection->getTable($tableName)->getColumn($key);
  4107. if ($field->getName() == $pk->getName()) {
  4108. unset($record->$key);
  4109. }
  4110. }
  4111. }
  4112. }
  4113. public function hasTable(String $table): bool
  4114. {
  4115. return $this->reflection->hasTable($table);
  4116. }
  4117. public function getType(String $table): String
  4118. {
  4119. return $this->reflection->getType($table);
  4120. }
  4121. public function create(String $tableName, /* object */ $record, array $params)
  4122. {
  4123. $this->sanitizeRecord($tableName, $record, '');
  4124. $table = $this->reflection->getTable($tableName);
  4125. $columnValues = $this->columns->getValues($table, true, $record, $params);
  4126. return $this->db->createSingle($table, $columnValues);
  4127. }
  4128. public function read(String $tableName, String $id, array $params) /*: ?object*/
  4129. {
  4130. $table = $this->reflection->getTable($tableName);
  4131. $this->joiner->addMandatoryColumns($table, $params);
  4132. $columnNames = $this->columns->getNames($table, true, $params);
  4133. $record = $this->db->selectSingle($table, $columnNames, $id);
  4134. if ($record == null) {
  4135. return null;
  4136. }
  4137. $records = array($record);
  4138. $this->joiner->addJoins($table, $records, $params, $this->db);
  4139. return $records[0];
  4140. }
  4141. public function update(String $tableName, String $id, /* object */ $record, array $params)
  4142. {
  4143. $this->sanitizeRecord($tableName, $record, $id);
  4144. $table = $this->reflection->getTable($tableName);
  4145. $columnValues = $this->columns->getValues($table, true, $record, $params);
  4146. return $this->db->updateSingle($table, $columnValues, $id);
  4147. }
  4148. public function delete(String $tableName, String $id, array $params)
  4149. {
  4150. $table = $this->reflection->getTable($tableName);
  4151. return $this->db->deleteSingle($table, $id);
  4152. }
  4153. public function increment(String $tableName, String $id, /* object */ $record, array $params)
  4154. {
  4155. $this->sanitizeRecord($tableName, $record, $id);
  4156. $table = $this->reflection->getTable($tableName);
  4157. $columnValues = $this->columns->getValues($table, true, $record, $params);
  4158. return $this->db->incrementSingle($table, $columnValues, $id);
  4159. }
  4160. public function _list(String $tableName, array $params): ListDocument
  4161. {
  4162. $table = $this->reflection->getTable($tableName);
  4163. $this->joiner->addMandatoryColumns($table, $params);
  4164. $columnNames = $this->columns->getNames($table, true, $params);
  4165. $condition = $this->filters->getCombinedConditions($table, $params);
  4166. $columnOrdering = $this->ordering->getColumnOrdering($table, $params);
  4167. if (!$this->pagination->hasPage($params)) {
  4168. $offset = 0;
  4169. $limit = $this->pagination->getResultSize($params);
  4170. $count = 0;
  4171. } else {
  4172. $offset = $this->pagination->getPageOffset($params);
  4173. $limit = $this->pagination->getPageSize($params);
  4174. $count = $this->db->selectCount($table, $condition);
  4175. }
  4176. $records = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, $offset, $limit);
  4177. $this->joiner->addJoins($table, $records, $params, $this->db);
  4178. return new ListDocument($records, $count);
  4179. }
  4180. }
  4181. // file: src/Tqdev/PhpCrudApi/Record/RelationJoiner.php
  4182. class RelationJoiner
  4183. {
  4184. private $reflection;
  4185. private $columns;
  4186. public function __construct(ReflectionService $reflection, ColumnIncluder $columns)
  4187. {
  4188. $this->reflection = $reflection;
  4189. $this->columns = $columns;
  4190. }
  4191. public function addMandatoryColumns(ReflectedTable $table, array &$params) /*: void*/
  4192. {
  4193. if (!isset($params['join']) || !isset($params['include'])) {
  4194. return;
  4195. }
  4196. $params['mandatory'] = array();
  4197. foreach ($params['join'] as $tableNames) {
  4198. $t1 = $table;
  4199. foreach (explode(',', $tableNames) as $tableName) {
  4200. if (!$this->reflection->hasTable($tableName)) {
  4201. continue;
  4202. }
  4203. $t2 = $this->reflection->getTable($tableName);
  4204. $fks1 = $t1->getFksTo($t2->getName());
  4205. $t3 = $this->hasAndBelongsToMany($t1, $t2);
  4206. if ($t3 != null || count($fks1) > 0) {
  4207. $params['mandatory'][] = $t2->getName() . '.' . $t2->getPk()->getName();
  4208. }
  4209. foreach ($fks1 as $fk) {
  4210. $params['mandatory'][] = $t1->getName() . '.' . $fk->getName();
  4211. }
  4212. $fks2 = $t2->getFksTo($t1->getName());
  4213. if ($t3 != null || count($fks2) > 0) {
  4214. $params['mandatory'][] = $t1->getName() . '.' . $t1->getPk()->getName();
  4215. }
  4216. foreach ($fks2 as $fk) {
  4217. $params['mandatory'][] = $t2->getName() . '.' . $fk->getName();
  4218. }
  4219. $t1 = $t2;
  4220. }
  4221. }
  4222. }
  4223. private function getJoinsAsPathTree(array $params): PathTree
  4224. {
  4225. $joins = new PathTree();
  4226. if (isset($params['join'])) {
  4227. foreach ($params['join'] as $tableNames) {
  4228. $path = array();
  4229. foreach (explode(',', $tableNames) as $tableName) {
  4230. $t = $this->reflection->getTable($tableName);
  4231. if ($t != null) {
  4232. $path[] = $t->getName();
  4233. }
  4234. }
  4235. $joins->put($path, true);
  4236. }
  4237. }
  4238. return $joins;
  4239. }
  4240. public function addJoins(ReflectedTable $table, array &$records, array $params, GenericDB $db) /*: void*/
  4241. {
  4242. $joins = $this->getJoinsAsPathTree($params);
  4243. $this->addJoinsForTables($table, $joins, $records, $params, $db);
  4244. }
  4245. private function hasAndBelongsToMany(ReflectedTable $t1, ReflectedTable $t2) /*: ?ReflectedTable*/
  4246. {
  4247. foreach ($this->reflection->getTableNames() as $tableName) {
  4248. $t3 = $this->reflection->getTable($tableName);
  4249. if (count($t3->getFksTo($t1->getName())) > 0 && count($t3->getFksTo($t2->getName())) > 0) {
  4250. return $t3;
  4251. }
  4252. }
  4253. return null;
  4254. }
  4255. private function addJoinsForTables(ReflectedTable $t1, PathTree $joins, array &$records, array $params, GenericDB $db)
  4256. {
  4257. foreach ($joins->getKeys() as $t2Name) {
  4258. $t2 = $this->reflection->getTable($t2Name);
  4259. $belongsTo = count($t1->getFksTo($t2->getName())) > 0;
  4260. $hasMany = count($t2->getFksTo($t1->getName())) > 0;
  4261. $t3 = $this->hasAndBelongsToMany($t1, $t2);
  4262. $hasAndBelongsToMany = ($t3 != null);
  4263. $newRecords = array();
  4264. $fkValues = null;
  4265. $pkValues = null;
  4266. $habtmValues = null;
  4267. if ($belongsTo) {
  4268. $fkValues = $this->getFkEmptyValues($t1, $t2, $records);
  4269. $this->addFkRecords($t2, $fkValues, $params, $db, $newRecords);
  4270. }
  4271. if ($hasMany) {
  4272. $pkValues = $this->getPkEmptyValues($t1, $records);
  4273. $this->addPkRecords($t1, $t2, $pkValues, $params, $db, $newRecords);
  4274. }
  4275. if ($hasAndBelongsToMany) {
  4276. $habtmValues = $this->getHabtmEmptyValues($t1, $t2, $t3, $db, $records);
  4277. $this->addFkRecords($t2, $habtmValues->fkValues, $params, $db, $newRecords);
  4278. }
  4279. $this->addJoinsForTables($t2, $joins->get($t2Name), $newRecords, $params, $db);
  4280. if ($fkValues != null) {
  4281. $this->fillFkValues($t2, $newRecords, $fkValues);
  4282. $this->setFkValues($t1, $t2, $records, $fkValues);
  4283. }
  4284. if ($pkValues != null) {
  4285. $this->fillPkValues($t1, $t2, $newRecords, $pkValues);
  4286. $this->setPkValues($t1, $t2, $records, $pkValues);
  4287. }
  4288. if ($habtmValues != null) {
  4289. $this->fillFkValues($t2, $newRecords, $habtmValues->fkValues);
  4290. $this->setHabtmValues($t1, $t3, $records, $habtmValues);
  4291. }
  4292. }
  4293. }
  4294. private function getFkEmptyValues(ReflectedTable $t1, ReflectedTable $t2, array $records): array
  4295. {
  4296. $fkValues = array();
  4297. $fks = $t1->getFksTo($t2->getName());
  4298. foreach ($fks as $fk) {
  4299. $fkName = $fk->getName();
  4300. foreach ($records as $record) {
  4301. if (isset($record[$fkName])) {
  4302. $fkValue = $record[$fkName];
  4303. $fkValues[$fkValue] = null;
  4304. }
  4305. }
  4306. }
  4307. return $fkValues;
  4308. }
  4309. private function addFkRecords(ReflectedTable $t2, array $fkValues, array $params, GenericDB $db, array &$records) /*: void*/
  4310. {
  4311. $columnNames = $this->columns->getNames($t2, false, $params);
  4312. $fkIds = array_keys($fkValues);
  4313. foreach ($db->selectMultiple($t2, $columnNames, $fkIds) as $record) {
  4314. $records[] = $record;
  4315. }
  4316. }
  4317. private function fillFkValues(ReflectedTable $t2, array $fkRecords, array &$fkValues) /*: void*/
  4318. {
  4319. $pkName = $t2->getPk()->getName();
  4320. foreach ($fkRecords as $fkRecord) {
  4321. $pkValue = $fkRecord[$pkName];
  4322. $fkValues[$pkValue] = $fkRecord;
  4323. }
  4324. }
  4325. private function setFkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $fkValues) /*: void*/
  4326. {
  4327. $fks = $t1->getFksTo($t2->getName());
  4328. foreach ($fks as $fk) {
  4329. $fkName = $fk->getName();
  4330. foreach ($records as $i => $record) {
  4331. if (isset($record[$fkName])) {
  4332. $key = $record[$fkName];
  4333. $records[$i][$fkName] = $fkValues[$key];
  4334. }
  4335. }
  4336. }
  4337. }
  4338. private function getPkEmptyValues(ReflectedTable $t1, array $records): array
  4339. {
  4340. $pkValues = array();
  4341. $pkName = $t1->getPk()->getName();
  4342. foreach ($records as $record) {
  4343. $key = $record[$pkName];
  4344. $pkValues[$key] = array();
  4345. }
  4346. return $pkValues;
  4347. }
  4348. private function addPkRecords(ReflectedTable $t1, ReflectedTable $t2, array $pkValues, array $params, GenericDB $db, array &$records) /*: void*/
  4349. {
  4350. $fks = $t2->getFksTo($t1->getName());
  4351. $columnNames = $this->columns->getNames($t2, false, $params);
  4352. $pkValueKeys = implode(',', array_keys($pkValues));
  4353. $conditions = array();
  4354. foreach ($fks as $fk) {
  4355. $conditions[] = new ColumnCondition($fk, 'in', $pkValueKeys);
  4356. }
  4357. $condition = OrCondition::fromArray($conditions);
  4358. foreach ($db->selectAllUnordered($t2, $columnNames, $condition) as $record) {
  4359. $records[] = $record;
  4360. }
  4361. }
  4362. private function fillPkValues(ReflectedTable $t1, ReflectedTable $t2, array $pkRecords, array &$pkValues) /*: void*/
  4363. {
  4364. $fks = $t2->getFksTo($t1->getName());
  4365. foreach ($fks as $fk) {
  4366. $fkName = $fk->getName();
  4367. foreach ($pkRecords as $pkRecord) {
  4368. $key = $pkRecord[$fkName];
  4369. if (isset($pkValues[$key])) {
  4370. $pkValues[$key][] = $pkRecord;
  4371. }
  4372. }
  4373. }
  4374. }
  4375. private function setPkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $pkValues) /*: void*/
  4376. {
  4377. $pkName = $t1->getPk()->getName();
  4378. $t2Name = $t2->getName();
  4379. foreach ($records as $i => $record) {
  4380. $key = $record[$pkName];
  4381. $records[$i][$t2Name] = $pkValues[$key];
  4382. }
  4383. }
  4384. private function getHabtmEmptyValues(ReflectedTable $t1, ReflectedTable $t2, ReflectedTable $t3, GenericDB $db, array $records): HabtmValues
  4385. {
  4386. $pkValues = $this->getPkEmptyValues($t1, $records);
  4387. $fkValues = array();
  4388. $fk1 = $t3->getFksTo($t1->getName())[0];
  4389. $fk2 = $t3->getFksTo($t2->getName())[0];
  4390. $fk1Name = $fk1->getName();
  4391. $fk2Name = $fk2->getName();
  4392. $columnNames = array($fk1Name, $fk2Name);
  4393. $pkIds = implode(',', array_keys($pkValues));
  4394. $condition = new ColumnCondition($t3->getColumn($fk1Name), 'in', $pkIds);
  4395. $records = $db->selectAllUnordered($t3, $columnNames, $condition);
  4396. foreach ($records as $record) {
  4397. $val1 = $record[$fk1Name];
  4398. $val2 = $record[$fk2Name];
  4399. $pkValues[$val1][] = $val2;
  4400. $fkValues[$val2] = null;
  4401. }
  4402. return new HabtmValues($pkValues, $fkValues);
  4403. }
  4404. private function setHabtmValues(ReflectedTable $t1, ReflectedTable $t3, array &$records, HabtmValues $habtmValues) /*: void*/
  4405. {
  4406. $pkName = $t1->getPk()->getName();
  4407. $t3Name = $t3->getName();
  4408. foreach ($records as $i => $record) {
  4409. $key = $record[$pkName];
  4410. $val = array();
  4411. $fks = $habtmValues->pkValues[$key];
  4412. foreach ($fks as $fk) {
  4413. $val[] = $habtmValues->fkValues[$fk];
  4414. }
  4415. $records[$i][$t3Name] = $val;
  4416. }
  4417. }
  4418. }
  4419. // file: src/Tqdev/PhpCrudApi/Record/RequestUtils.php
  4420. class RequestUtils
  4421. {
  4422. private $reflection;
  4423. public function __construct(ReflectionService $reflection)
  4424. {
  4425. $this->reflection = $reflection;
  4426. }
  4427. public function getOperation(Request $request): String
  4428. {
  4429. $method = $request->getMethod();
  4430. $path = $request->getPathSegment(1);
  4431. $hasPk = $request->getPathSegment(3) != '';
  4432. switch ($path) {
  4433. case 'openapi':
  4434. return 'document';
  4435. case 'columns':
  4436. return $method == 'get' ? 'reflect' : 'remodel';
  4437. case 'records':
  4438. switch ($method) {
  4439. case 'POST':
  4440. return 'create';
  4441. case 'GET':
  4442. return $hasPk ? 'read' : 'list';
  4443. case 'PUT':
  4444. return 'update';
  4445. case 'DELETE':
  4446. return 'delete';
  4447. case 'PATCH':
  4448. return 'increment';
  4449. }
  4450. }
  4451. return 'unknown';
  4452. }
  4453. private function getJoinTables(String $tableName, array $parameters): array
  4454. {
  4455. $uniqueTableNames = array();
  4456. $uniqueTableNames[$tableName] = true;
  4457. if (isset($parameters['join'])) {
  4458. foreach ($parameters['join'] as $parameter) {
  4459. $tableNames = explode(',', trim($parameter));
  4460. foreach ($tableNames as $tableName) {
  4461. $uniqueTableNames[$tableName] = true;
  4462. }
  4463. }
  4464. }
  4465. return array_keys($uniqueTableNames);
  4466. }
  4467. public function getTableNames(Request $request): array
  4468. {
  4469. $path = $request->getPathSegment(1);
  4470. $tableName = $request->getPathSegment(2);
  4471. $allTableNames = $this->reflection->getTableNames();
  4472. switch ($path) {
  4473. case 'openapi':
  4474. return $allTableNames;
  4475. case 'columns':
  4476. return $tableName ? [$tableName] : $allTableNames;
  4477. case 'records':
  4478. return $this->getJoinTables($tableName, $request->getParams());
  4479. }
  4480. return $allTableNames;
  4481. }
  4482. }
  4483. // file: src/Tqdev/PhpCrudApi/Api.php
  4484. class Api
  4485. {
  4486. private $router;
  4487. private $responder;
  4488. private $debug;
  4489. public function __construct(Config $config)
  4490. {
  4491. $db = new GenericDB(
  4492. $config->getDriver(),
  4493. $config->getAddress(),
  4494. $config->getPort(),
  4495. $config->getDatabase(),
  4496. $config->getUsername(),
  4497. $config->getPassword()
  4498. );
  4499. $cache = CacheFactory::create($config);
  4500. $reflection = new ReflectionService($db, $cache, $config->getCacheTime());
  4501. $responder = new Responder();
  4502. $router = new SimpleRouter($responder, $cache, $config->getCacheTime());
  4503. foreach ($config->getMiddlewares() as $middleware => $properties) {
  4504. switch ($middleware) {
  4505. case 'cors':
  4506. new CorsMiddleware($router, $responder, $properties);
  4507. break;
  4508. case 'firewall':
  4509. new FirewallMiddleware($router, $responder, $properties);
  4510. break;
  4511. case 'basicAuth':
  4512. new BasicAuthMiddleware($router, $responder, $properties);
  4513. break;
  4514. case 'jwtAuth':
  4515. new JwtAuthMiddleware($router, $responder, $properties);
  4516. break;
  4517. case 'fileUpload':
  4518. new FileUploadMiddleware($router, $responder, $properties);
  4519. break;
  4520. case 'validation':
  4521. new ValidationMiddleware($router, $responder, $properties, $reflection);
  4522. break;
  4523. case 'sanitation':
  4524. new SanitationMiddleware($router, $responder, $properties, $reflection);
  4525. break;
  4526. case 'multiTenancy':
  4527. new MultiTenancyMiddleware($router, $responder, $properties, $reflection);
  4528. break;
  4529. case 'authorization':
  4530. new AuthorizationMiddleware($router, $responder, $properties, $reflection);
  4531. break;
  4532. case 'xsrf':
  4533. new XsrfMiddleware($router, $responder, $properties);
  4534. break;
  4535. case 'customization':
  4536. new CustomizationMiddleware($router, $responder, $properties, $reflection);
  4537. break;
  4538. }
  4539. }
  4540. foreach ($config->getControllers() as $controller) {
  4541. switch ($controller) {
  4542. case 'records':
  4543. $records = new RecordService($db, $reflection);
  4544. new RecordController($router, $responder, $records);
  4545. break;
  4546. case 'columns':
  4547. $definition = new DefinitionService($db, $reflection);
  4548. new ColumnController($router, $responder, $reflection, $definition);
  4549. break;
  4550. case 'cache':
  4551. new CacheController($router, $responder, $cache);
  4552. break;
  4553. case 'openapi':
  4554. $openApi = new OpenApiService($reflection, $config->getOpenApiBase());
  4555. new OpenApiController($router, $responder, $openApi);
  4556. break;
  4557. }
  4558. }
  4559. $this->router = $router;
  4560. $this->responder = $responder;
  4561. $this->debug = $config->getDebug();
  4562. }
  4563. public function handle(Request $request): Response
  4564. {
  4565. $response = null;
  4566. try {
  4567. $response = $this->router->route($request);
  4568. } catch (\Throwable $e) {
  4569. if ($e instanceof \PDOException) {
  4570. if (strpos(strtolower($e->getMessage()), 'duplicate') !== false) {
  4571. return $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
  4572. }
  4573. if (strpos(strtolower($e->getMessage()), 'default value') !== false) {
  4574. return $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  4575. }
  4576. if (strpos(strtolower($e->getMessage()), 'allow nulls') !== false) {
  4577. return $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  4578. }
  4579. if (strpos(strtolower($e->getMessage()), 'constraint') !== false) {
  4580. return $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  4581. }
  4582. }
  4583. $response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
  4584. if ($this->debug) {
  4585. $response->addHeader('X-Debug-Info', 'Exception in ' . $e->getFile() . ' on line ' . $e->getLine());
  4586. }
  4587. }
  4588. return $response;
  4589. }
  4590. }
  4591. // file: src/Tqdev/PhpCrudApi/Config.php
  4592. class Config
  4593. {
  4594. private $values = [
  4595. 'driver' => null,
  4596. 'address' => 'localhost',
  4597. 'port' => null,
  4598. 'username' => null,
  4599. 'password' => null,
  4600. 'database' => null,
  4601. 'middlewares' => 'cors',
  4602. 'controllers' => 'records,openapi',
  4603. 'cacheType' => 'TempFile',
  4604. 'cachePath' => '',
  4605. 'cacheTime' => 10,
  4606. 'debug' => false,
  4607. 'openApiBase' => '{"info":{"title":"PHP-CRUD-API","version":"1.0.0"}}',
  4608. ];
  4609. private function getDefaultDriver(array $values): String
  4610. {
  4611. if (isset($values['driver'])) {
  4612. return $values['driver'];
  4613. }
  4614. return 'mysql';
  4615. }
  4616. private function getDefaultPort(String $driver): int
  4617. {
  4618. switch ($driver) {
  4619. case 'mysql':return 3306;
  4620. case 'pgsql':return 5432;
  4621. case 'sqlsrv':return 1433;
  4622. }
  4623. }
  4624. private function getDefaultAddress(String $driver): String
  4625. {
  4626. switch ($driver) {
  4627. case 'mysql':return 'localhost';
  4628. case 'pgsql':return 'localhost';
  4629. case 'sqlsrv':return 'localhost';
  4630. }
  4631. }
  4632. private function getDriverDefaults(String $driver): array
  4633. {
  4634. return [
  4635. 'driver' => $driver,
  4636. 'address' => $this->getDefaultAddress($driver),
  4637. 'port' => $this->getDefaultPort($driver),
  4638. ];
  4639. }
  4640. public function __construct(array $values)
  4641. {
  4642. $driver = $this->getDefaultDriver($values);
  4643. $defaults = $this->getDriverDefaults($driver);
  4644. $newValues = array_merge($this->values, $defaults, $values);
  4645. $newValues = $this->parseMiddlewares($newValues);
  4646. $diff = array_diff_key($newValues, $this->values);
  4647. if (!empty($diff)) {
  4648. $key = array_keys($diff)[0];
  4649. throw new \Exception("Config has invalid value '$key'");
  4650. }
  4651. $this->values = $newValues;
  4652. }
  4653. private function parseMiddlewares(array $values): array
  4654. {
  4655. $newValues = array();
  4656. $properties = array();
  4657. $middlewares = array_map('trim', explode(',', $values['middlewares']));
  4658. foreach ($middlewares as $middleware) {
  4659. $properties[$middleware] = [];
  4660. }
  4661. foreach ($values as $key => $value) {
  4662. if (strpos($key, '.') === false) {
  4663. $newValues[$key] = $value;
  4664. } else {
  4665. list($middleware, $key2) = explode('.', $key, 2);
  4666. if (isset($properties[$middleware])) {
  4667. $properties[$middleware][$key2] = $value;
  4668. } else {
  4669. throw new \Exception("Config has invalid value '$key'");
  4670. }
  4671. }
  4672. }
  4673. $newValues['middlewares'] = array_reverse($properties, true);
  4674. return $newValues;
  4675. }
  4676. public function getDriver(): String
  4677. {
  4678. return $this->values['driver'];
  4679. }
  4680. public function getAddress(): String
  4681. {
  4682. return $this->values['address'];
  4683. }
  4684. public function getPort(): int
  4685. {
  4686. return $this->values['port'];
  4687. }
  4688. public function getUsername(): String
  4689. {
  4690. return $this->values['username'];
  4691. }
  4692. public function getPassword(): String
  4693. {
  4694. return $this->values['password'];
  4695. }
  4696. public function getDatabase(): String
  4697. {
  4698. return $this->values['database'];
  4699. }
  4700. public function getMiddlewares(): array
  4701. {
  4702. return $this->values['middlewares'];
  4703. }
  4704. public function getControllers(): array
  4705. {
  4706. return array_map('trim', explode(',', $this->values['controllers']));
  4707. }
  4708. public function getCacheType(): String
  4709. {
  4710. return $this->values['cacheType'];
  4711. }
  4712. public function getCachePath(): String
  4713. {
  4714. return $this->values['cachePath'];
  4715. }
  4716. public function getCacheTime(): int
  4717. {
  4718. return $this->values['cacheTime'];
  4719. }
  4720. public function getDebug(): String
  4721. {
  4722. return $this->values['debug'];
  4723. }
  4724. public function getOpenApiBase(): array
  4725. {
  4726. return json_decode($this->values['openApiBase'], true);
  4727. }
  4728. }
  4729. // file: src/Tqdev/PhpCrudApi/Request.php
  4730. class Request
  4731. {
  4732. private $method;
  4733. private $path;
  4734. private $pathSegments;
  4735. private $params;
  4736. private $body;
  4737. private $headers;
  4738. private $highPerformance;
  4739. public function __construct(String $method = null, String $path = null, String $query = null, array $headers = null, String $body = null, bool $highPerformance = true)
  4740. {
  4741. $this->parseMethod($method);
  4742. $this->parsePath($path);
  4743. $this->parseParams($query);
  4744. $this->parseHeaders($headers);
  4745. $this->parseBody($body);
  4746. $this->highPerformance = $highPerformance;
  4747. }
  4748. private function parseMethod(String $method = null)
  4749. {
  4750. if (!$method) {
  4751. if (isset($_SERVER['REQUEST_METHOD'])) {
  4752. $method = $_SERVER['REQUEST_METHOD'];
  4753. } else {
  4754. $method = 'GET';
  4755. }
  4756. }
  4757. $this->method = $method;
  4758. }
  4759. private function parsePath(String $path = null)
  4760. {
  4761. if (!$path) {
  4762. if (isset($_SERVER['PATH_INFO'])) {
  4763. $path = $_SERVER['PATH_INFO'];
  4764. } else {
  4765. $path = '/';
  4766. }
  4767. }
  4768. $this->path = $path;
  4769. $this->pathSegments = explode('/', $path);
  4770. }
  4771. private function parseParams(String $query = null)
  4772. {
  4773. if (!$query) {
  4774. if (isset($_SERVER['QUERY_STRING'])) {
  4775. $query = $_SERVER['QUERY_STRING'];
  4776. } else {
  4777. $query = '';
  4778. }
  4779. }
  4780. $query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
  4781. parse_str($query, $this->params);
  4782. }
  4783. private function parseHeaders(array $headers = null)
  4784. {
  4785. if (!$headers) {
  4786. $headers = array();
  4787. if (!$this->highPerformance) {
  4788. foreach ($_SERVER as $name => $value) {
  4789. if (substr($name, 0, 5) == 'HTTP_') {
  4790. $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
  4791. $headers[$key] = $value;
  4792. }
  4793. }
  4794. }
  4795. }
  4796. $this->headers = $headers;
  4797. }
  4798. private function decodeBody(String $body) /*: ?object*/
  4799. {
  4800. $first = substr($body, 0, 1);
  4801. if ($first == '[' || $first == '{') {
  4802. $object = json_decode($body);
  4803. $causeCode = json_last_error();
  4804. if ($causeCode !== JSON_ERROR_NONE) {
  4805. $object = null;
  4806. }
  4807. } else {
  4808. parse_str($body, $input);
  4809. foreach ($input as $key => $value) {
  4810. if (substr($key, -9) == '__is_null') {
  4811. $input[substr($key, 0, -9)] = null;
  4812. unset($input[$key]);
  4813. }
  4814. }
  4815. $object = (object) $input;
  4816. }
  4817. return $object;
  4818. }
  4819. private function parseBody(String $body = null) /*: void*/
  4820. {
  4821. if ($body) {
  4822. $object = $this->decodeBody($body);
  4823. } else {
  4824. if (!empty($_FILES)) {
  4825. $object = (object) $_POST;
  4826. } else {
  4827. $input = file_get_contents('php://input');
  4828. $object = $this->decodeBody($input);
  4829. }
  4830. }
  4831. $this->body = $object;
  4832. }
  4833. public function getMethod(): String
  4834. {
  4835. return $this->method;
  4836. }
  4837. public function getPath(): String
  4838. {
  4839. return $this->path;
  4840. }
  4841. public function getPathSegment(int $part): String
  4842. {
  4843. if ($part < 0 || $part >= count($this->pathSegments)) {
  4844. return '';
  4845. }
  4846. return $this->pathSegments[$part];
  4847. }
  4848. public function getParams(): array
  4849. {
  4850. return $this->params;
  4851. }
  4852. public function getBody() /*: ?array*/
  4853. {
  4854. return $this->body;
  4855. }
  4856. public function setBody($body) /*: void*/
  4857. {
  4858. $this->body = $body;
  4859. }
  4860. public function addHeader(String $key, String $value)
  4861. {
  4862. $this->headers[$key] = $value;
  4863. }
  4864. public function getHeader(String $key): String
  4865. {
  4866. if (isset($this->headers[$key])) {
  4867. return $this->headers[$key];
  4868. }
  4869. if ($this->highPerformance) {
  4870. $serverKey = 'HTTP_' . strtoupper(str_replace('-', '_', $key));
  4871. if (isset($_SERVER[$serverKey])) {
  4872. return $_SERVER[$serverKey];
  4873. }
  4874. }
  4875. return '';
  4876. }
  4877. public function getHeaders(): array
  4878. {
  4879. return $this->headers;
  4880. }
  4881. public static function fromString(String $request): Request
  4882. {
  4883. $parts = explode("\n\n", trim($request), 2);
  4884. $head = $parts[0];
  4885. $body = isset($parts[1]) ? $parts[1] : null;
  4886. $lines = explode("\n", $head);
  4887. $line = explode(' ', trim(array_shift($lines)), 2);
  4888. $method = $line[0];
  4889. $url = isset($line[1]) ? $line[1] : '';
  4890. $path = parse_url($url, PHP_URL_PATH);
  4891. $query = parse_url($url, PHP_URL_QUERY);
  4892. $headers = array();
  4893. foreach ($lines as $line) {
  4894. list($key, $value) = explode(':', $line, 2);
  4895. $headers[$key] = trim($value);
  4896. }
  4897. return new Request($method, $path, $query, $headers, $body);
  4898. }
  4899. public function getUploadedFiles(): array
  4900. {
  4901. return $_FILES;
  4902. }
  4903. }
  4904. // file: src/Tqdev/PhpCrudApi/Response.php
  4905. class Response
  4906. {
  4907. const OK = 200;
  4908. const UNAUTHORIZED = 401;
  4909. const FORBIDDEN = 403;
  4910. const NOT_FOUND = 404;
  4911. const METHOD_NOT_ALLOWED = 405;
  4912. const CONFLICT = 409;
  4913. const UNPROCESSABLE_ENTITY = 422;
  4914. const INTERNAL_SERVER_ERROR = 500;
  4915. private $status;
  4916. private $headers;
  4917. private $body;
  4918. public function __construct(int $status, $body)
  4919. {
  4920. $this->status = $status;
  4921. $this->headers = array();
  4922. $this->parseBody($body);
  4923. }
  4924. private function parseBody($body)
  4925. {
  4926. if ($body === '') {
  4927. $this->body = '';
  4928. } else {
  4929. $data = json_encode($body, JSON_UNESCAPED_UNICODE);
  4930. $this->addHeader('Content-Type', 'application/json');
  4931. $this->addHeader('Content-Length', strlen($data));
  4932. $this->body = $data;
  4933. }
  4934. }
  4935. public function getStatus(): int
  4936. {
  4937. return $this->status;
  4938. }
  4939. public function getBody(): String
  4940. {
  4941. return $this->body;
  4942. }
  4943. public function addHeader(String $key, String $value)
  4944. {
  4945. $this->headers[$key] = $value;
  4946. }
  4947. public function getHeader(String $key): String
  4948. {
  4949. if (isset($this->headers[$key])) {
  4950. return $this->headers[$key];
  4951. }
  4952. return null;
  4953. }
  4954. public function getHeaders(): array
  4955. {
  4956. return $this->headers;
  4957. }
  4958. public function output()
  4959. {
  4960. http_response_code($this->getStatus());
  4961. foreach ($this->headers as $key => $value) {
  4962. header("$key: $value");
  4963. }
  4964. echo $this->getBody();
  4965. }
  4966. public function __toString(): String
  4967. {
  4968. $str = "$this->status\n";
  4969. foreach ($this->headers as $key => $value) {
  4970. $str .= "$key: $value\n";
  4971. }
  4972. if ($this->body !== '') {
  4973. $str .= "\n";
  4974. $str .= "$this->body\n";
  4975. }
  4976. return $str;
  4977. }
  4978. }
  4979. // file: src/index.php
  4980. $config = new Config([
  4981. 'username' => 'php-crud-api',
  4982. 'password' => 'php-crud-api',
  4983. 'database' => 'php-crud-api',
  4984. ]);
  4985. $request = new Request();
  4986. $api = new Api($config);
  4987. $response = $api->handle($request);
  4988. $response->output();