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 200KB

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