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
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

api.php 244KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472
  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/Psr/Http/Message/MessageInterface.php
  9. interface MessageInterface
  10. {
  11. public function getProtocolVersion();
  12. public function withProtocolVersion($version);
  13. public function getHeaders();
  14. public function hasHeader($name);
  15. public function getHeader($name);
  16. public function getHeaderLine($name);
  17. public function withHeader($name, $value);
  18. public function withAddedHeader($name, $value);
  19. public function withoutHeader($name);
  20. public function getBody();
  21. public function withBody(StreamInterface $body);
  22. }
  23. // file: src/Psr/Http/Message/RequestFactoryInterface.php
  24. interface RequestFactoryInterface
  25. {
  26. public function createRequest(string $method, $uri): RequestInterface;
  27. }
  28. // file: src/Psr/Http/Message/RequestInterface.php
  29. interface RequestInterface extends MessageInterface
  30. {
  31. public function getRequestTarget();
  32. public function withRequestTarget($requestTarget);
  33. public function getMethod();
  34. public function withMethod($method);
  35. public function getUri();
  36. public function withUri(UriInterface $uri, $preserveHost = false);
  37. }
  38. // file: src/Psr/Http/Message/ResponseFactoryInterface.php
  39. interface ResponseFactoryInterface
  40. {
  41. public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface;
  42. }
  43. // file: src/Psr/Http/Message/ResponseInterface.php
  44. interface ResponseInterface extends MessageInterface
  45. {
  46. public function getStatusCode();
  47. public function withStatus($code, $reasonPhrase = '');
  48. public function getReasonPhrase();
  49. }
  50. // file: src/Psr/Http/Message/ServerRequestFactoryInterface.php
  51. interface ServerRequestFactoryInterface
  52. {
  53. public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface;
  54. }
  55. // file: src/Psr/Http/Message/ServerRequestInterface.php
  56. interface ServerRequestInterface extends RequestInterface
  57. {
  58. public function getServerParams();
  59. public function getCookieParams();
  60. public function withCookieParams(array $cookies);
  61. public function getQueryParams();
  62. public function withQueryParams(array $query);
  63. public function getUploadedFiles();
  64. public function withUploadedFiles(array $uploadedFiles);
  65. public function getParsedBody();
  66. public function withParsedBody($data);
  67. public function getAttributes();
  68. public function getAttribute($name, $default = null);
  69. public function withAttribute($name, $value);
  70. public function withoutAttribute($name);
  71. }
  72. // file: src/Psr/Http/Message/StreamFactoryInterface.php
  73. interface StreamFactoryInterface
  74. {
  75. public function createStream(string $content = ''): StreamInterface;
  76. public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface;
  77. public function createStreamFromResource($resource): StreamInterface;
  78. }
  79. // file: src/Psr/Http/Message/StreamInterface.php
  80. interface StreamInterface
  81. {
  82. public function __toString();
  83. public function close();
  84. public function detach();
  85. public function getSize();
  86. public function tell();
  87. public function eof();
  88. public function isSeekable();
  89. public function seek($offset, $whence = SEEK_SET);
  90. public function rewind();
  91. public function isWritable();
  92. public function write($string);
  93. public function isReadable();
  94. public function read($length);
  95. public function getContents();
  96. public function getMetadata($key = null);
  97. }
  98. // file: src/Psr/Http/Message/UploadedFileFactoryInterface.php
  99. interface UploadedFileFactoryInterface
  100. {
  101. public function createUploadedFile(
  102. StreamInterface $stream,
  103. int $size = null,
  104. int $error = \UPLOAD_ERR_OK,
  105. string $clientFilename = null,
  106. string $clientMediaType = null
  107. ): UploadedFileInterface;
  108. }
  109. // file: src/Psr/Http/Message/UploadedFileInterface.php
  110. interface UploadedFileInterface
  111. {
  112. public function getStream();
  113. public function moveTo($targetPath);
  114. public function getSize();
  115. public function getError();
  116. public function getClientFilename();
  117. public function getClientMediaType();
  118. }
  119. // file: src/Psr/Http/Message/UriFactoryInterface.php
  120. interface UriFactoryInterface
  121. {
  122. public function createUri(string $uri = ''): UriInterface;
  123. }
  124. // file: src/Psr/Http/Message/UriInterface.php
  125. interface UriInterface
  126. {
  127. public function getScheme();
  128. public function getAuthority();
  129. public function getUserInfo();
  130. public function getHost();
  131. public function getPort();
  132. public function getPath();
  133. public function getQuery();
  134. public function getFragment();
  135. public function withScheme($scheme);
  136. public function withUserInfo($user, $password = null);
  137. public function withHost($host);
  138. public function withPort($port);
  139. public function withPath($path);
  140. public function withQuery($query);
  141. public function withFragment($fragment);
  142. public function __toString();
  143. }
  144. // file: src/Psr/Http/Server/MiddlewareInterface.php
  145. interface MiddlewareInterface
  146. {
  147. public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
  148. }
  149. // file: src/Psr/Http/Server/RequestHandlerInterface.php
  150. interface RequestHandlerInterface
  151. {
  152. public function handle(ServerRequestInterface $request): ResponseInterface;
  153. }
  154. // file: src/Nyholm/Psr7/Factory/Psr17Factory.php
  155. final class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface
  156. {
  157. public function createRequest(string $method, $uri): RequestInterface
  158. {
  159. return new Request($method, $uri);
  160. }
  161. public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
  162. {
  163. return new Response($code, [], null, '1.1', $reasonPhrase);
  164. }
  165. public function createStream(string $content = ''): StreamInterface
  166. {
  167. return Stream::create($content);
  168. }
  169. public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
  170. {
  171. $resource = @\fopen($filename, $mode);
  172. if (false === $resource) {
  173. if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'])) {
  174. throw new \InvalidArgumentException('The mode ' . $mode . ' is invalid.');
  175. }
  176. throw new \RuntimeException('The file ' . $filename . ' cannot be opened.');
  177. }
  178. return Stream::create($resource);
  179. }
  180. public function createStreamFromResource($resource): StreamInterface
  181. {
  182. return Stream::create($resource);
  183. }
  184. public function createUploadedFile(StreamInterface $stream, int $size = null, int $error = \UPLOAD_ERR_OK, string $clientFilename = null, string $clientMediaType = null): UploadedFileInterface
  185. {
  186. if (null === $size) {
  187. $size = $stream->getSize();
  188. }
  189. return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
  190. }
  191. public function createUri(string $uri = ''): UriInterface
  192. {
  193. return new Uri($uri);
  194. }
  195. public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
  196. {
  197. return new ServerRequest($method, $uri, [], null, '1.1', $serverParams);
  198. }
  199. }
  200. // file: src/Nyholm/Psr7/MessageTrait.php
  201. trait MessageTrait
  202. {
  203. private $headers = [];
  204. private $headerNames = [];
  205. private $protocol = '1.1';
  206. private $stream;
  207. public function getProtocolVersion(): string
  208. {
  209. return $this->protocol;
  210. }
  211. public function withProtocolVersion($version): self
  212. {
  213. if ($this->protocol === $version) {
  214. return $this;
  215. }
  216. $new = clone $this;
  217. $new->protocol = $version;
  218. return $new;
  219. }
  220. public function getHeaders(): array
  221. {
  222. return $this->headers;
  223. }
  224. public function hasHeader($header): bool
  225. {
  226. return isset($this->headerNames[\strtolower($header)]);
  227. }
  228. public function getHeader($header): array
  229. {
  230. $header = \strtolower($header);
  231. if (!isset($this->headerNames[$header])) {
  232. return [];
  233. }
  234. $header = $this->headerNames[$header];
  235. return $this->headers[$header];
  236. }
  237. public function getHeaderLine($header): string
  238. {
  239. return \implode(', ', $this->getHeader($header));
  240. }
  241. public function withHeader($header, $value): self
  242. {
  243. $value = $this->validateAndTrimHeader($header, $value);
  244. $normalized = \strtolower($header);
  245. $new = clone $this;
  246. if (isset($new->headerNames[$normalized])) {
  247. unset($new->headers[$new->headerNames[$normalized]]);
  248. }
  249. $new->headerNames[$normalized] = $header;
  250. $new->headers[$header] = $value;
  251. return $new;
  252. }
  253. public function withAddedHeader($header, $value): self
  254. {
  255. if (!\is_string($header) || '' === $header) {
  256. throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
  257. }
  258. $new = clone $this;
  259. $new->setHeaders([$header => $value]);
  260. return $new;
  261. }
  262. public function withoutHeader($header): self
  263. {
  264. $normalized = \strtolower($header);
  265. if (!isset($this->headerNames[$normalized])) {
  266. return $this;
  267. }
  268. $header = $this->headerNames[$normalized];
  269. $new = clone $this;
  270. unset($new->headers[$header], $new->headerNames[$normalized]);
  271. return $new;
  272. }
  273. public function getBody(): StreamInterface
  274. {
  275. if (null === $this->stream) {
  276. $this->stream = Stream::create('');
  277. }
  278. return $this->stream;
  279. }
  280. public function withBody(StreamInterface $body): self
  281. {
  282. if ($body === $this->stream) {
  283. return $this;
  284. }
  285. $new = clone $this;
  286. $new->stream = $body;
  287. return $new;
  288. }
  289. private function setHeaders(array $headers): void
  290. {
  291. foreach ($headers as $header => $value) {
  292. $value = $this->validateAndTrimHeader($header, $value);
  293. $normalized = \strtolower($header);
  294. if (isset($this->headerNames[$normalized])) {
  295. $header = $this->headerNames[$normalized];
  296. $this->headers[$header] = \array_merge($this->headers[$header], $value);
  297. } else {
  298. $this->headerNames[$normalized] = $header;
  299. $this->headers[$header] = $value;
  300. }
  301. }
  302. }
  303. private function validateAndTrimHeader($header, $values): array
  304. {
  305. if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) {
  306. throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
  307. }
  308. if (!\is_array($values)) {
  309. if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) {
  310. throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
  311. }
  312. return [\trim((string) $values, " \t")];
  313. }
  314. if (empty($values)) {
  315. throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given.');
  316. }
  317. $returnValues = [];
  318. foreach ($values as $v) {
  319. if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) {
  320. throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
  321. }
  322. $returnValues[] = \trim((string) $v, " \t");
  323. }
  324. return $returnValues;
  325. }
  326. }
  327. // file: src/Nyholm/Psr7/Request.php
  328. final class Request implements RequestInterface
  329. {
  330. use MessageTrait;
  331. use RequestTrait;
  332. public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1')
  333. {
  334. if (!($uri instanceof UriInterface)) {
  335. $uri = new Uri($uri);
  336. }
  337. $this->method = $method;
  338. $this->uri = $uri;
  339. $this->setHeaders($headers);
  340. $this->protocol = $version;
  341. if (!$this->hasHeader('Host')) {
  342. $this->updateHostFromUri();
  343. }
  344. if ('' !== $body && null !== $body) {
  345. $this->stream = Stream::create($body);
  346. }
  347. }
  348. }
  349. // file: src/Nyholm/Psr7/RequestTrait.php
  350. trait RequestTrait
  351. {
  352. private $method;
  353. private $requestTarget;
  354. private $uri;
  355. public function getRequestTarget(): string
  356. {
  357. if (null !== $this->requestTarget) {
  358. return $this->requestTarget;
  359. }
  360. if ('' === $target = $this->uri->getPath()) {
  361. $target = '/';
  362. }
  363. if ('' !== $this->uri->getQuery()) {
  364. $target .= '?' . $this->uri->getQuery();
  365. }
  366. return $target;
  367. }
  368. public function withRequestTarget($requestTarget): self
  369. {
  370. if (\preg_match('#\s#', $requestTarget)) {
  371. throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace');
  372. }
  373. $new = clone $this;
  374. $new->requestTarget = $requestTarget;
  375. return $new;
  376. }
  377. public function getMethod(): string
  378. {
  379. return $this->method;
  380. }
  381. public function withMethod($method): self
  382. {
  383. if (!\is_string($method)) {
  384. throw new \InvalidArgumentException('Method must be a string');
  385. }
  386. $new = clone $this;
  387. $new->method = $method;
  388. return $new;
  389. }
  390. public function getUri(): UriInterface
  391. {
  392. return $this->uri;
  393. }
  394. public function withUri(UriInterface $uri, $preserveHost = false): self
  395. {
  396. if ($uri === $this->uri) {
  397. return $this;
  398. }
  399. $new = clone $this;
  400. $new->uri = $uri;
  401. if (!$preserveHost || !$this->hasHeader('Host')) {
  402. $new->updateHostFromUri();
  403. }
  404. return $new;
  405. }
  406. private function updateHostFromUri(): void
  407. {
  408. if ('' === $host = $this->uri->getHost()) {
  409. return;
  410. }
  411. if (null !== ($port = $this->uri->getPort())) {
  412. $host .= ':' . $port;
  413. }
  414. if (isset($this->headerNames['host'])) {
  415. $header = $this->headerNames['host'];
  416. } else {
  417. $this->headerNames['host'] = $header = 'Host';
  418. }
  419. $this->headers = [$header => [$host]] + $this->headers;
  420. }
  421. }
  422. // file: src/Nyholm/Psr7/Response.php
  423. final class Response implements ResponseInterface
  424. {
  425. use MessageTrait;
  426. private const PHRASES = [
  427. 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing',
  428. 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported',
  429. 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect',
  430. 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons',
  431. 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required',
  432. ];
  433. private $reasonPhrase = '';
  434. private $statusCode;
  435. public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', string $reason = null)
  436. {
  437. if ('' !== $body && null !== $body) {
  438. $this->stream = Stream::create($body);
  439. }
  440. $this->statusCode = $status;
  441. $this->setHeaders($headers);
  442. if (null === $reason && isset(self::PHRASES[$this->statusCode])) {
  443. $this->reasonPhrase = self::PHRASES[$status];
  444. } else {
  445. $this->reasonPhrase = $reason;
  446. }
  447. $this->protocol = $version;
  448. }
  449. public function getStatusCode(): int
  450. {
  451. return $this->statusCode;
  452. }
  453. public function getReasonPhrase(): string
  454. {
  455. return $this->reasonPhrase;
  456. }
  457. public function withStatus($code, $reasonPhrase = ''): self
  458. {
  459. if (!\is_int($code) && !\is_string($code)) {
  460. throw new \InvalidArgumentException('Status code has to be an integer');
  461. }
  462. $code = (int) $code;
  463. if ($code < 100 || $code > 599) {
  464. throw new \InvalidArgumentException('Status code has to be an integer between 100 and 599');
  465. }
  466. $new = clone $this;
  467. $new->statusCode = $code;
  468. if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$new->statusCode])) {
  469. $reasonPhrase = self::PHRASES[$new->statusCode];
  470. }
  471. $new->reasonPhrase = $reasonPhrase;
  472. return $new;
  473. }
  474. }
  475. // file: src/Nyholm/Psr7/ServerRequest.php
  476. final class ServerRequest implements ServerRequestInterface
  477. {
  478. use MessageTrait;
  479. use RequestTrait;
  480. private $attributes = [];
  481. private $cookieParams = [];
  482. private $parsedBody;
  483. private $queryParams = [];
  484. private $serverParams;
  485. private $uploadedFiles = [];
  486. public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = [])
  487. {
  488. $this->serverParams = $serverParams;
  489. if (!($uri instanceof UriInterface)) {
  490. $uri = new Uri($uri);
  491. }
  492. $this->method = $method;
  493. $this->uri = $uri;
  494. $this->setHeaders($headers);
  495. $this->protocol = $version;
  496. if (!$this->hasHeader('Host')) {
  497. $this->updateHostFromUri();
  498. }
  499. if ('' !== $body && null !== $body) {
  500. $this->stream = Stream::create($body);
  501. }
  502. }
  503. public function getServerParams(): array
  504. {
  505. return $this->serverParams;
  506. }
  507. public function getUploadedFiles(): array
  508. {
  509. return $this->uploadedFiles;
  510. }
  511. public function withUploadedFiles(array $uploadedFiles)
  512. {
  513. $new = clone $this;
  514. $new->uploadedFiles = $uploadedFiles;
  515. return $new;
  516. }
  517. public function getCookieParams(): array
  518. {
  519. return $this->cookieParams;
  520. }
  521. public function withCookieParams(array $cookies)
  522. {
  523. $new = clone $this;
  524. $new->cookieParams = $cookies;
  525. return $new;
  526. }
  527. public function getQueryParams(): array
  528. {
  529. return $this->queryParams;
  530. }
  531. public function withQueryParams(array $query)
  532. {
  533. $new = clone $this;
  534. $new->queryParams = $query;
  535. return $new;
  536. }
  537. public function getParsedBody()
  538. {
  539. return $this->parsedBody;
  540. }
  541. public function withParsedBody($data)
  542. {
  543. if (!\is_array($data) && !\is_object($data) && null !== $data) {
  544. throw new \InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null');
  545. }
  546. $new = clone $this;
  547. $new->parsedBody = $data;
  548. return $new;
  549. }
  550. public function getAttributes(): array
  551. {
  552. return $this->attributes;
  553. }
  554. public function getAttribute($attribute, $default = null)
  555. {
  556. if (false === \array_key_exists($attribute, $this->attributes)) {
  557. return $default;
  558. }
  559. return $this->attributes[$attribute];
  560. }
  561. public function withAttribute($attribute, $value): self
  562. {
  563. $new = clone $this;
  564. $new->attributes[$attribute] = $value;
  565. return $new;
  566. }
  567. public function withoutAttribute($attribute): self
  568. {
  569. if (false === \array_key_exists($attribute, $this->attributes)) {
  570. return $this;
  571. }
  572. $new = clone $this;
  573. unset($new->attributes[$attribute]);
  574. return $new;
  575. }
  576. }
  577. // file: src/Nyholm/Psr7/Stream.php
  578. final class Stream implements StreamInterface
  579. {
  580. private $stream;
  581. private $seekable;
  582. private $readable;
  583. private $writable;
  584. private $uri;
  585. private $size;
  586. private const READ_WRITE_HASH = [
  587. 'read' => [
  588. 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
  589. 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
  590. 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
  591. 'x+t' => true, 'c+t' => true, 'a+' => true,
  592. ],
  593. 'write' => [
  594. 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
  595. 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
  596. 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
  597. 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
  598. ],
  599. ];
  600. private function __construct()
  601. {
  602. }
  603. public static function create($body = ''): StreamInterface
  604. {
  605. if ($body instanceof StreamInterface) {
  606. return $body;
  607. }
  608. if (\is_string($body)) {
  609. $resource = \fopen('php://temp', 'rw+');
  610. \fwrite($resource, $body);
  611. $body = $resource;
  612. }
  613. if (\is_resource($body)) {
  614. $new = new self();
  615. $new->stream = $body;
  616. $meta = \stream_get_meta_data($new->stream);
  617. $new->seekable = $meta['seekable'];
  618. $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
  619. $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
  620. $new->uri = $new->getMetadata('uri');
  621. return $new;
  622. }
  623. throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.');
  624. }
  625. public function __destruct()
  626. {
  627. $this->close();
  628. }
  629. public function __toString(): string
  630. {
  631. try {
  632. if ($this->isSeekable()) {
  633. $this->seek(0);
  634. }
  635. return $this->getContents();
  636. } catch (\Exception $e) {
  637. return '';
  638. }
  639. }
  640. public function close(): void
  641. {
  642. if (isset($this->stream)) {
  643. if (\is_resource($this->stream)) {
  644. \fclose($this->stream);
  645. }
  646. $this->detach();
  647. }
  648. }
  649. public function detach()
  650. {
  651. if (!isset($this->stream)) {
  652. return null;
  653. }
  654. $result = $this->stream;
  655. unset($this->stream);
  656. $this->size = $this->uri = null;
  657. $this->readable = $this->writable = $this->seekable = false;
  658. return $result;
  659. }
  660. public function getSize(): ?int
  661. {
  662. if (null !== $this->size) {
  663. return $this->size;
  664. }
  665. if (!isset($this->stream)) {
  666. return null;
  667. }
  668. if ($this->uri) {
  669. \clearstatcache(true, $this->uri);
  670. }
  671. $stats = \fstat($this->stream);
  672. if (isset($stats['size'])) {
  673. $this->size = $stats['size'];
  674. return $this->size;
  675. }
  676. return null;
  677. }
  678. public function tell(): int
  679. {
  680. if (false === $result = \ftell($this->stream)) {
  681. throw new \RuntimeException('Unable to determine stream position');
  682. }
  683. return $result;
  684. }
  685. public function eof(): bool
  686. {
  687. return !$this->stream || \feof($this->stream);
  688. }
  689. public function isSeekable(): bool
  690. {
  691. return $this->seekable;
  692. }
  693. public function seek($offset, $whence = \SEEK_SET): void
  694. {
  695. if (!$this->seekable) {
  696. throw new \RuntimeException('Stream is not seekable');
  697. }
  698. if (-1 === \fseek($this->stream, $offset, $whence)) {
  699. throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . \var_export($whence, true));
  700. }
  701. }
  702. public function rewind(): void
  703. {
  704. $this->seek(0);
  705. }
  706. public function isWritable(): bool
  707. {
  708. return $this->writable;
  709. }
  710. public function write($string): int
  711. {
  712. if (!$this->writable) {
  713. throw new \RuntimeException('Cannot write to a non-writable stream');
  714. }
  715. $this->size = null;
  716. if (false === $result = \fwrite($this->stream, $string)) {
  717. throw new \RuntimeException('Unable to write to stream');
  718. }
  719. return $result;
  720. }
  721. public function isReadable(): bool
  722. {
  723. return $this->readable;
  724. }
  725. public function read($length): string
  726. {
  727. if (!$this->readable) {
  728. throw new \RuntimeException('Cannot read from non-readable stream');
  729. }
  730. return \fread($this->stream, $length);
  731. }
  732. public function getContents(): string
  733. {
  734. if (!isset($this->stream)) {
  735. throw new \RuntimeException('Unable to read stream contents');
  736. }
  737. if (false === $contents = \stream_get_contents($this->stream)) {
  738. throw new \RuntimeException('Unable to read stream contents');
  739. }
  740. return $contents;
  741. }
  742. public function getMetadata($key = null)
  743. {
  744. if (!isset($this->stream)) {
  745. return $key ? null : [];
  746. }
  747. $meta = \stream_get_meta_data($this->stream);
  748. if (null === $key) {
  749. return $meta;
  750. }
  751. return $meta[$key] ?? null;
  752. }
  753. }
  754. // file: src/Nyholm/Psr7/UploadedFile.php
  755. final class UploadedFile implements UploadedFileInterface
  756. {
  757. private const ERRORS = [
  758. \UPLOAD_ERR_OK => 1,
  759. \UPLOAD_ERR_INI_SIZE => 1,
  760. \UPLOAD_ERR_FORM_SIZE => 1,
  761. \UPLOAD_ERR_PARTIAL => 1,
  762. \UPLOAD_ERR_NO_FILE => 1,
  763. \UPLOAD_ERR_NO_TMP_DIR => 1,
  764. \UPLOAD_ERR_CANT_WRITE => 1,
  765. \UPLOAD_ERR_EXTENSION => 1,
  766. ];
  767. private $clientFilename;
  768. private $clientMediaType;
  769. private $error;
  770. private $file;
  771. private $moved = false;
  772. private $size;
  773. private $stream;
  774. public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null)
  775. {
  776. if (false === \is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) {
  777. throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants.');
  778. }
  779. if (false === \is_int($size)) {
  780. throw new \InvalidArgumentException('Upload file size must be an integer');
  781. }
  782. if (null !== $clientFilename && !\is_string($clientFilename)) {
  783. throw new \InvalidArgumentException('Upload file client filename must be a string or null');
  784. }
  785. if (null !== $clientMediaType && !\is_string($clientMediaType)) {
  786. throw new \InvalidArgumentException('Upload file client media type must be a string or null');
  787. }
  788. $this->error = $errorStatus;
  789. $this->size = $size;
  790. $this->clientFilename = $clientFilename;
  791. $this->clientMediaType = $clientMediaType;
  792. if (\UPLOAD_ERR_OK === $this->error) {
  793. if (\is_string($streamOrFile)) {
  794. $this->file = $streamOrFile;
  795. } elseif (\is_resource($streamOrFile)) {
  796. $this->stream = Stream::create($streamOrFile);
  797. } elseif ($streamOrFile instanceof StreamInterface) {
  798. $this->stream = $streamOrFile;
  799. } else {
  800. throw new \InvalidArgumentException('Invalid stream or file provided for UploadedFile');
  801. }
  802. }
  803. }
  804. private function validateActive(): void
  805. {
  806. if (\UPLOAD_ERR_OK !== $this->error) {
  807. throw new \RuntimeException('Cannot retrieve stream due to upload error');
  808. }
  809. if ($this->moved) {
  810. throw new \RuntimeException('Cannot retrieve stream after it has already been moved');
  811. }
  812. }
  813. public function getStream(): StreamInterface
  814. {
  815. $this->validateActive();
  816. if ($this->stream instanceof StreamInterface) {
  817. return $this->stream;
  818. }
  819. $resource = \fopen($this->file, 'r');
  820. return Stream::create($resource);
  821. }
  822. public function moveTo($targetPath): void
  823. {
  824. $this->validateActive();
  825. if (!\is_string($targetPath) || '' === $targetPath) {
  826. throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
  827. }
  828. if (null !== $this->file) {
  829. $this->moved = 'cli' === \PHP_SAPI ? \rename($this->file, $targetPath) : \move_uploaded_file($this->file, $targetPath);
  830. } else {
  831. $stream = $this->getStream();
  832. if ($stream->isSeekable()) {
  833. $stream->rewind();
  834. }
  835. $dest = Stream::create(\fopen($targetPath, 'w'));
  836. while (!$stream->eof()) {
  837. if (!$dest->write($stream->read(1048576))) {
  838. break;
  839. }
  840. }
  841. $this->moved = true;
  842. }
  843. if (false === $this->moved) {
  844. throw new \RuntimeException(\sprintf('Uploaded file could not be moved to %s', $targetPath));
  845. }
  846. }
  847. public function getSize(): int
  848. {
  849. return $this->size;
  850. }
  851. public function getError(): int
  852. {
  853. return $this->error;
  854. }
  855. public function getClientFilename(): ?string
  856. {
  857. return $this->clientFilename;
  858. }
  859. public function getClientMediaType(): ?string
  860. {
  861. return $this->clientMediaType;
  862. }
  863. }
  864. // file: src/Nyholm/Psr7/Uri.php
  865. final class Uri implements UriInterface
  866. {
  867. private const SCHEMES = ['http' => 80, 'https' => 443];
  868. private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
  869. private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
  870. private $scheme = '';
  871. private $userInfo = '';
  872. private $host = '';
  873. private $port;
  874. private $path = '';
  875. private $query = '';
  876. private $fragment = '';
  877. public function __construct(string $uri = '')
  878. {
  879. if ('' !== $uri) {
  880. if (false === $parts = \parse_url($uri)) {
  881. throw new \InvalidArgumentException("Unable to parse URI: $uri");
  882. }
  883. $this->scheme = isset($parts['scheme']) ? \strtolower($parts['scheme']) : '';
  884. $this->userInfo = $parts['user'] ?? '';
  885. $this->host = isset($parts['host']) ? \strtolower($parts['host']) : '';
  886. $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
  887. $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
  888. $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
  889. $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
  890. if (isset($parts['pass'])) {
  891. $this->userInfo .= ':' . $parts['pass'];
  892. }
  893. }
  894. }
  895. public function __toString(): string
  896. {
  897. return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
  898. }
  899. public function getScheme(): string
  900. {
  901. return $this->scheme;
  902. }
  903. public function getAuthority(): string
  904. {
  905. if ('' === $this->host) {
  906. return '';
  907. }
  908. $authority = $this->host;
  909. if ('' !== $this->userInfo) {
  910. $authority = $this->userInfo . '@' . $authority;
  911. }
  912. if (null !== $this->port) {
  913. $authority .= ':' . $this->port;
  914. }
  915. return $authority;
  916. }
  917. public function getUserInfo(): string
  918. {
  919. return $this->userInfo;
  920. }
  921. public function getHost(): string
  922. {
  923. return $this->host;
  924. }
  925. public function getPort(): ?int
  926. {
  927. return $this->port;
  928. }
  929. public function getPath(): string
  930. {
  931. return $this->path;
  932. }
  933. public function getQuery(): string
  934. {
  935. return $this->query;
  936. }
  937. public function getFragment(): string
  938. {
  939. return $this->fragment;
  940. }
  941. public function withScheme($scheme): self
  942. {
  943. if (!\is_string($scheme)) {
  944. throw new \InvalidArgumentException('Scheme must be a string');
  945. }
  946. if ($this->scheme === $scheme = \strtolower($scheme)) {
  947. return $this;
  948. }
  949. $new = clone $this;
  950. $new->scheme = $scheme;
  951. $new->port = $new->filterPort($new->port);
  952. return $new;
  953. }
  954. public function withUserInfo($user, $password = null): self
  955. {
  956. $info = $user;
  957. if (null !== $password && '' !== $password) {
  958. $info .= ':' . $password;
  959. }
  960. if ($this->userInfo === $info) {
  961. return $this;
  962. }
  963. $new = clone $this;
  964. $new->userInfo = $info;
  965. return $new;
  966. }
  967. public function withHost($host): self
  968. {
  969. if (!\is_string($host)) {
  970. throw new \InvalidArgumentException('Host must be a string');
  971. }
  972. if ($this->host === $host = \strtolower($host)) {
  973. return $this;
  974. }
  975. $new = clone $this;
  976. $new->host = $host;
  977. return $new;
  978. }
  979. public function withPort($port): self
  980. {
  981. if ($this->port === $port = $this->filterPort($port)) {
  982. return $this;
  983. }
  984. $new = clone $this;
  985. $new->port = $port;
  986. return $new;
  987. }
  988. public function withPath($path): self
  989. {
  990. if ($this->path === $path = $this->filterPath($path)) {
  991. return $this;
  992. }
  993. $new = clone $this;
  994. $new->path = $path;
  995. return $new;
  996. }
  997. public function withQuery($query): self
  998. {
  999. if ($this->query === $query = $this->filterQueryAndFragment($query)) {
  1000. return $this;
  1001. }
  1002. $new = clone $this;
  1003. $new->query = $query;
  1004. return $new;
  1005. }
  1006. public function withFragment($fragment): self
  1007. {
  1008. if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) {
  1009. return $this;
  1010. }
  1011. $new = clone $this;
  1012. $new->fragment = $fragment;
  1013. return $new;
  1014. }
  1015. private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string
  1016. {
  1017. $uri = '';
  1018. if ('' !== $scheme) {
  1019. $uri .= $scheme . ':';
  1020. }
  1021. if ('' !== $authority) {
  1022. $uri .= '//' . $authority;
  1023. }
  1024. if ('' !== $path) {
  1025. if ('/' !== $path[0]) {
  1026. if ('' !== $authority) {
  1027. $path = '/' . $path;
  1028. }
  1029. } elseif (isset($path[1]) && '/' === $path[1]) {
  1030. if ('' === $authority) {
  1031. $path = '/' . \ltrim($path, '/');
  1032. }
  1033. }
  1034. $uri .= $path;
  1035. }
  1036. if ('' !== $query) {
  1037. $uri .= '?' . $query;
  1038. }
  1039. if ('' !== $fragment) {
  1040. $uri .= '#' . $fragment;
  1041. }
  1042. return $uri;
  1043. }
  1044. private static function isNonStandardPort(string $scheme, int $port): bool
  1045. {
  1046. return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme];
  1047. }
  1048. private function filterPort($port): ?int
  1049. {
  1050. if (null === $port) {
  1051. return null;
  1052. }
  1053. $port = (int) $port;
  1054. if (1 > $port || 0xffff < $port) {
  1055. throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 1 and 65535', $port));
  1056. }
  1057. return self::isNonStandardPort($this->scheme, $port) ? $port : null;
  1058. }
  1059. private function filterPath($path): string
  1060. {
  1061. if (!\is_string($path)) {
  1062. throw new \InvalidArgumentException('Path must be a string');
  1063. }
  1064. return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path);
  1065. }
  1066. private function filterQueryAndFragment($str): string
  1067. {
  1068. if (!\is_string($str)) {
  1069. throw new \InvalidArgumentException('Query and fragment must be a string');
  1070. }
  1071. return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str);
  1072. }
  1073. private static function rawurlencodeMatchZero(array $match): string
  1074. {
  1075. return \rawurlencode($match[0]);
  1076. }
  1077. }
  1078. // file: src/Nyholm/Psr7Server/ServerRequestCreator.php
  1079. final class ServerRequestCreator implements ServerRequestCreatorInterface
  1080. {
  1081. private $serverRequestFactory;
  1082. private $uriFactory;
  1083. private $uploadedFileFactory;
  1084. private $streamFactory;
  1085. public function __construct(
  1086. ServerRequestFactoryInterface $serverRequestFactory,
  1087. UriFactoryInterface $uriFactory,
  1088. UploadedFileFactoryInterface $uploadedFileFactory,
  1089. StreamFactoryInterface $streamFactory
  1090. ) {
  1091. $this->serverRequestFactory = $serverRequestFactory;
  1092. $this->uriFactory = $uriFactory;
  1093. $this->uploadedFileFactory = $uploadedFileFactory;
  1094. $this->streamFactory = $streamFactory;
  1095. }
  1096. public function fromGlobals(): ServerRequestInterface
  1097. {
  1098. $server = $_SERVER;
  1099. if (false === isset($server['REQUEST_METHOD'])) {
  1100. $server['REQUEST_METHOD'] = 'GET';
  1101. }
  1102. $headers = \function_exists('getallheaders') ? getallheaders() : static::getHeadersFromServer($_SERVER);
  1103. return $this->fromArrays($server, $headers, $_COOKIE, $_GET, $_POST, $_FILES, \fopen('php://input', 'r') ?: null);
  1104. }
  1105. public function fromArrays(array $server, array $headers = [], array $cookie = [], array $get = [], array $post = [], array $files = [], $body = null): ServerRequestInterface
  1106. {
  1107. $method = $this->getMethodFromEnv($server);
  1108. $uri = $this->getUriFromEnvWithHTTP($server);
  1109. $protocol = isset($server['SERVER_PROTOCOL']) ? \str_replace('HTTP/', '', $server['SERVER_PROTOCOL']) : '1.1';
  1110. $serverRequest = $this->serverRequestFactory->createServerRequest($method, $uri, $server);
  1111. foreach ($headers as $name => $value) {
  1112. $serverRequest = $serverRequest->withAddedHeader($name, $value);
  1113. }
  1114. $serverRequest = $serverRequest
  1115. ->withProtocolVersion($protocol)
  1116. ->withCookieParams($cookie)
  1117. ->withQueryParams($get)
  1118. ->withParsedBody($post)
  1119. ->withUploadedFiles($this->normalizeFiles($files));
  1120. if (null === $body) {
  1121. return $serverRequest;
  1122. }
  1123. if (\is_resource($body)) {
  1124. $body = $this->streamFactory->createStreamFromResource($body);
  1125. } elseif (\is_string($body)) {
  1126. $body = $this->streamFactory->createStream($body);
  1127. } elseif (!$body instanceof StreamInterface) {
  1128. throw new \InvalidArgumentException('The $body parameter to ServerRequestCreator::fromArrays must be string, resource or StreamInterface');
  1129. }
  1130. return $serverRequest->withBody($body);
  1131. }
  1132. public static function getHeadersFromServer(array $server): array
  1133. {
  1134. $headers = [];
  1135. foreach ($server as $key => $value) {
  1136. if (0 === \strpos($key, 'REDIRECT_')) {
  1137. $key = \substr($key, 9);
  1138. if (\array_key_exists($key, $server)) {
  1139. continue;
  1140. }
  1141. }
  1142. if ($value && 0 === \strpos($key, 'HTTP_')) {
  1143. $name = \strtr(\strtolower(\substr($key, 5)), '_', '-');
  1144. $headers[$name] = $value;
  1145. continue;
  1146. }
  1147. if ($value && 0 === \strpos($key, 'CONTENT_')) {
  1148. $name = 'content-'.\strtolower(\substr($key, 8));
  1149. $headers[$name] = $value;
  1150. continue;
  1151. }
  1152. }
  1153. return $headers;
  1154. }
  1155. private function getMethodFromEnv(array $environment): string
  1156. {
  1157. if (false === isset($environment['REQUEST_METHOD'])) {
  1158. throw new \InvalidArgumentException('Cannot determine HTTP method');
  1159. }
  1160. return $environment['REQUEST_METHOD'];
  1161. }
  1162. private function getUriFromEnvWithHTTP(array $environment): UriInterface
  1163. {
  1164. $uri = $this->createUriFromArray($environment);
  1165. if (empty($uri->getScheme())) {
  1166. $uri = $uri->withScheme('http');
  1167. }
  1168. return $uri;
  1169. }
  1170. private function normalizeFiles(array $files): array
  1171. {
  1172. $normalized = [];
  1173. foreach ($files as $key => $value) {
  1174. if ($value instanceof UploadedFileInterface) {
  1175. $normalized[$key] = $value;
  1176. } elseif (\is_array($value) && isset($value['tmp_name'])) {
  1177. $normalized[$key] = $this->createUploadedFileFromSpec($value);
  1178. } elseif (\is_array($value)) {
  1179. $normalized[$key] = $this->normalizeFiles($value);
  1180. } else {
  1181. throw new \InvalidArgumentException('Invalid value in files specification');
  1182. }
  1183. }
  1184. return $normalized;
  1185. }
  1186. private function createUploadedFileFromSpec(array $value)
  1187. {
  1188. if (\is_array($value['tmp_name'])) {
  1189. return $this->normalizeNestedFileSpec($value);
  1190. }
  1191. try {
  1192. $stream = $this->streamFactory->createStreamFromFile($value['tmp_name']);
  1193. } catch (\RuntimeException $e) {
  1194. $stream = $this->streamFactory->createStream();
  1195. }
  1196. return $this->uploadedFileFactory->createUploadedFile(
  1197. $stream,
  1198. (int) $value['size'],
  1199. (int) $value['error'],
  1200. $value['name'],
  1201. $value['type']
  1202. );
  1203. }
  1204. private function normalizeNestedFileSpec(array $files = []): array
  1205. {
  1206. $normalizedFiles = [];
  1207. foreach (\array_keys($files['tmp_name']) as $key) {
  1208. $spec = [
  1209. 'tmp_name' => $files['tmp_name'][$key],
  1210. 'size' => $files['size'][$key],
  1211. 'error' => $files['error'][$key],
  1212. 'name' => $files['name'][$key],
  1213. 'type' => $files['type'][$key],
  1214. ];
  1215. $normalizedFiles[$key] = $this->createUploadedFileFromSpec($spec);
  1216. }
  1217. return $normalizedFiles;
  1218. }
  1219. private function createUriFromArray(array $server): UriInterface
  1220. {
  1221. $uri = $this->uriFactory->createUri('');
  1222. if (isset($server['REQUEST_SCHEME'])) {
  1223. $uri = $uri->withScheme($server['REQUEST_SCHEME']);
  1224. } elseif (isset($server['HTTPS'])) {
  1225. $uri = $uri->withScheme('on' === $server['HTTPS'] ? 'https' : 'http');
  1226. }
  1227. if (isset($server['SERVER_PORT'])) {
  1228. $uri = $uri->withPort($server['SERVER_PORT']);
  1229. }
  1230. if (isset($server['HTTP_HOST'])) {
  1231. if (1 === \preg_match('/^(.+)\:(\d+)$/', $server['HTTP_HOST'], $matches)) {
  1232. $uri = $uri->withHost($matches[1])->withPort($matches[2]);
  1233. } else {
  1234. $uri = $uri->withHost($server['HTTP_HOST']);
  1235. }
  1236. } elseif (isset($server['SERVER_NAME'])) {
  1237. $uri = $uri->withHost($server['SERVER_NAME']);
  1238. }
  1239. if (isset($server['REQUEST_URI'])) {
  1240. $uri = $uri->withPath(\current(\explode('?', $server['REQUEST_URI'])));
  1241. }
  1242. if (isset($server['QUERY_STRING'])) {
  1243. $uri = $uri->withQuery($server['QUERY_STRING']);
  1244. }
  1245. return $uri;
  1246. }
  1247. }
  1248. // file: src/Nyholm/Psr7Server/ServerRequestCreatorInterface.php
  1249. interface ServerRequestCreatorInterface
  1250. {
  1251. public function fromGlobals(): ServerRequestInterface;
  1252. public function fromArrays(
  1253. array $server,
  1254. array $headers = [],
  1255. array $cookie = [],
  1256. array $get = [],
  1257. array $post = [],
  1258. array $files = [],
  1259. $body = null
  1260. ): ServerRequestInterface;
  1261. public static function getHeadersFromServer(array $server): array;
  1262. }
  1263. // file: src/Tqdev/PhpCrudApi/Cache/Cache.php
  1264. interface Cache
  1265. {
  1266. public function set(string $key, string $value, int $ttl = 0): bool;
  1267. public function get(string $key): string;
  1268. public function clear(): bool;
  1269. }
  1270. // file: src/Tqdev/PhpCrudApi/Cache/CacheFactory.php
  1271. class CacheFactory
  1272. {
  1273. const PREFIX = 'phpcrudapi-%s-%s-%s-';
  1274. private static function getPrefix(Config $config): string
  1275. {
  1276. $driver = $config->getDriver();
  1277. $database = $config->getDatabase();
  1278. $filehash = substr(md5(__FILE__), 0, 8);
  1279. return sprintf(self::PREFIX, $driver, $database, $filehash);
  1280. }
  1281. public static function create(Config $config): Cache
  1282. {
  1283. switch ($config->getCacheType()) {
  1284. case 'TempFile':
  1285. $cache = new TempFileCache(self::getPrefix($config), $config->getCachePath());
  1286. break;
  1287. case 'Redis':
  1288. $cache = new RedisCache(self::getPrefix($config), $config->getCachePath());
  1289. break;
  1290. case 'Memcache':
  1291. $cache = new MemcacheCache(self::getPrefix($config), $config->getCachePath());
  1292. break;
  1293. case 'Memcached':
  1294. $cache = new MemcachedCache(self::getPrefix($config), $config->getCachePath());
  1295. break;
  1296. default:
  1297. $cache = new NoCache();
  1298. }
  1299. return $cache;
  1300. }
  1301. }
  1302. // file: src/Tqdev/PhpCrudApi/Cache/MemcacheCache.php
  1303. class MemcacheCache implements Cache
  1304. {
  1305. protected $prefix;
  1306. protected $memcache;
  1307. public function __construct(string $prefix, string $config)
  1308. {
  1309. $this->prefix = $prefix;
  1310. if ($config == '') {
  1311. $address = 'localhost';
  1312. $port = 11211;
  1313. } elseif (strpos($config, ':') === false) {
  1314. $address = $config;
  1315. $port = 11211;
  1316. } else {
  1317. list($address, $port) = explode(':', $config);
  1318. }
  1319. $this->memcache = $this->create();
  1320. $this->memcache->addServer($address, $port);
  1321. }
  1322. protected function create() /*: \Memcache*/
  1323. {
  1324. return new \Memcache();
  1325. }
  1326. public function set(string $key, string $value, int $ttl = 0): bool
  1327. {
  1328. return $this->memcache->set($this->prefix . $key, $value, 0, $ttl);
  1329. }
  1330. public function get(string $key): string
  1331. {
  1332. return $this->memcache->get($this->prefix . $key) ?: '';
  1333. }
  1334. public function clear(): bool
  1335. {
  1336. return $this->memcache->flush();
  1337. }
  1338. }
  1339. // file: src/Tqdev/PhpCrudApi/Cache/MemcachedCache.php
  1340. class MemcachedCache extends MemcacheCache
  1341. {
  1342. protected function create() /*: \Memcached*/
  1343. {
  1344. return new \Memcached();
  1345. }
  1346. public function set(string $key, string $value, int $ttl = 0): bool
  1347. {
  1348. return $this->memcache->set($this->prefix . $key, $value, $ttl);
  1349. }
  1350. }
  1351. // file: src/Tqdev/PhpCrudApi/Cache/NoCache.php
  1352. class NoCache implements Cache
  1353. {
  1354. public function __construct()
  1355. {
  1356. }
  1357. public function set(string $key, string $value, int $ttl = 0): bool
  1358. {
  1359. return true;
  1360. }
  1361. public function get(string $key): string
  1362. {
  1363. return '';
  1364. }
  1365. public function clear(): bool
  1366. {
  1367. return true;
  1368. }
  1369. }
  1370. // file: src/Tqdev/PhpCrudApi/Cache/RedisCache.php
  1371. class RedisCache implements Cache
  1372. {
  1373. protected $prefix;
  1374. protected $redis;
  1375. public function __construct(string $prefix, string $config)
  1376. {
  1377. $this->prefix = $prefix;
  1378. if ($config == '') {
  1379. $config = '127.0.0.1';
  1380. }
  1381. $params = explode(':', $config, 6);
  1382. if (isset($params[3])) {
  1383. $params[3] = null;
  1384. }
  1385. $this->redis = new \Redis();
  1386. call_user_func_array(array($this->redis, 'pconnect'), $params);
  1387. }
  1388. public function set(string $key, string $value, int $ttl = 0): bool
  1389. {
  1390. return $this->redis->set($this->prefix . $key, $value, $ttl);
  1391. }
  1392. public function get(string $key): string
  1393. {
  1394. return $this->redis->get($this->prefix . $key) ?: '';
  1395. }
  1396. public function clear(): bool
  1397. {
  1398. return $this->redis->flushDb();
  1399. }
  1400. }
  1401. // file: src/Tqdev/PhpCrudApi/Cache/TempFileCache.php
  1402. class TempFileCache implements Cache
  1403. {
  1404. const SUFFIX = 'cache';
  1405. private $path;
  1406. private $segments;
  1407. public function __construct(string $prefix, string $config)
  1408. {
  1409. $this->segments = [];
  1410. $s = DIRECTORY_SEPARATOR;
  1411. $ps = PATH_SEPARATOR;
  1412. if ($config == '') {
  1413. $this->path = sys_get_temp_dir() . $s . $prefix . self::SUFFIX;
  1414. } elseif (strpos($config, $ps) === false) {
  1415. $this->path = $config;
  1416. } else {
  1417. list($path, $segments) = explode($ps, $config);
  1418. $this->path = $path;
  1419. $this->segments = explode(',', $segments);
  1420. }
  1421. if (file_exists($this->path) && is_dir($this->path)) {
  1422. $this->clean($this->path, array_filter($this->segments), strlen(md5('')), false);
  1423. }
  1424. }
  1425. private function getFileName(string $key): string
  1426. {
  1427. $s = DIRECTORY_SEPARATOR;
  1428. $md5 = md5($key);
  1429. $filename = rtrim($this->path, $s) . $s;
  1430. $i = 0;
  1431. foreach ($this->segments as $segment) {
  1432. $filename .= substr($md5, $i, $segment) . $s;
  1433. $i += $segment;
  1434. }
  1435. $filename .= substr($md5, $i);
  1436. return $filename;
  1437. }
  1438. public function set(string $key, string $value, int $ttl = 0): bool
  1439. {
  1440. $filename = $this->getFileName($key);
  1441. $dirname = dirname($filename);
  1442. if (!file_exists($dirname)) {
  1443. if (!mkdir($dirname, 0755, true)) {
  1444. return false;
  1445. }
  1446. }
  1447. $string = $ttl . '|' . $value;
  1448. return $this->filePutContents($filename, $string) !== false;
  1449. }
  1450. private function filePutContents($filename, $string)
  1451. {
  1452. return file_put_contents($filename, $string, LOCK_EX);
  1453. }
  1454. private function fileGetContents($filename)
  1455. {
  1456. $file = fopen($filename, 'rb');
  1457. if ($file === false) {
  1458. return false;
  1459. }
  1460. $lock = flock($file, LOCK_SH);
  1461. if (!$lock) {
  1462. fclose($file);
  1463. return false;
  1464. }
  1465. $string = '';
  1466. while (!feof($file)) {
  1467. $string .= fread($file, 8192);
  1468. }
  1469. flock($file, LOCK_UN);
  1470. fclose($file);
  1471. return $string;
  1472. }
  1473. private function getString($filename): string
  1474. {
  1475. $data = $this->fileGetContents($filename);
  1476. if ($data === false) {
  1477. return '';
  1478. }
  1479. list($ttl, $string) = explode('|', $data, 2);
  1480. if ($ttl > 0 && time() - filemtime($filename) > $ttl) {
  1481. return '';
  1482. }
  1483. return $string;
  1484. }
  1485. public function get(string $key): string
  1486. {
  1487. $filename = $this->getFileName($key);
  1488. if (!file_exists($filename)) {
  1489. return '';
  1490. }
  1491. $string = $this->getString($filename);
  1492. if ($string == null) {
  1493. return '';
  1494. }
  1495. return $string;
  1496. }
  1497. private function clean(string $path, array $segments, int $len, bool $all) /*: void*/
  1498. {
  1499. $entries = scandir($path);
  1500. foreach ($entries as $entry) {
  1501. if ($entry === '.' || $entry === '..') {
  1502. continue;
  1503. }
  1504. $filename = $path . DIRECTORY_SEPARATOR . $entry;
  1505. if (count($segments) == 0) {
  1506. if (strlen($entry) != $len) {
  1507. continue;
  1508. }
  1509. if (is_file($filename)) {
  1510. if ($all || $this->getString($filename) == null) {
  1511. unlink($filename);
  1512. }
  1513. }
  1514. } else {
  1515. if (strlen($entry) != $segments[0]) {
  1516. continue;
  1517. }
  1518. if (is_dir($filename)) {
  1519. $this->clean($filename, array_slice($segments, 1), $len - $segments[0], $all);
  1520. rmdir($filename);
  1521. }
  1522. }
  1523. }
  1524. }
  1525. public function clear(): bool
  1526. {
  1527. if (!file_exists($this->path) || !is_dir($this->path)) {
  1528. return false;
  1529. }
  1530. $this->clean($this->path, array_filter($this->segments), strlen(md5('')), true);
  1531. return true;
  1532. }
  1533. }
  1534. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedColumn.php
  1535. class ReflectedColumn implements \JsonSerializable
  1536. {
  1537. const DEFAULT_LENGTH = 255;
  1538. const DEFAULT_PRECISION = 19;
  1539. const DEFAULT_SCALE = 4;
  1540. private $name;
  1541. private $type;
  1542. private $length;
  1543. private $precision;
  1544. private $scale;
  1545. private $nullable;
  1546. private $pk;
  1547. private $fk;
  1548. public function __construct(string $name, string $type, int $length, int $precision, int $scale, bool $nullable, bool $pk, string $fk)
  1549. {
  1550. $this->name = $name;
  1551. $this->type = $type;
  1552. $this->length = $length;
  1553. $this->precision = $precision;
  1554. $this->scale = $scale;
  1555. $this->nullable = $nullable;
  1556. $this->pk = $pk;
  1557. $this->fk = $fk;
  1558. $this->sanitize();
  1559. }
  1560. public static function fromReflection(GenericReflection $reflection, array $columnResult): ReflectedColumn
  1561. {
  1562. $name = $columnResult['COLUMN_NAME'];
  1563. $length = (int) $columnResult['CHARACTER_MAXIMUM_LENGTH'];
  1564. $type = $reflection->toJdbcType($columnResult['DATA_TYPE'], $length);
  1565. $precision = (int) $columnResult['NUMERIC_PRECISION'];
  1566. $scale = (int) $columnResult['NUMERIC_SCALE'];
  1567. $nullable = in_array(strtoupper($columnResult['IS_NULLABLE']), ['TRUE', 'YES', 'T', 'Y', '1']);
  1568. $pk = false;
  1569. $fk = '';
  1570. return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
  1571. }
  1572. public static function fromJson( /* object */$json): ReflectedColumn
  1573. {
  1574. $name = $json->name;
  1575. $type = $json->type;
  1576. $length = isset($json->length) ? $json->length : 0;
  1577. $precision = isset($json->precision) ? $json->precision : 0;
  1578. $scale = isset($json->scale) ? $json->scale : 0;
  1579. $nullable = isset($json->nullable) ? $json->nullable : false;
  1580. $pk = isset($json->pk) ? $json->pk : false;
  1581. $fk = isset($json->fk) ? $json->fk : '';
  1582. return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
  1583. }
  1584. private function sanitize()
  1585. {
  1586. $this->length = $this->hasLength() ? $this->getLength() : 0;
  1587. $this->precision = $this->hasPrecision() ? $this->getPrecision() : 0;
  1588. $this->scale = $this->hasScale() ? $this->getScale() : 0;
  1589. }
  1590. public function getName(): string
  1591. {
  1592. return $this->name;
  1593. }
  1594. public function getNullable(): bool
  1595. {
  1596. return $this->nullable;
  1597. }
  1598. public function getType(): string
  1599. {
  1600. return $this->type;
  1601. }
  1602. public function getLength(): int
  1603. {
  1604. return $this->length ?: self::DEFAULT_LENGTH;
  1605. }
  1606. public function getPrecision(): int
  1607. {
  1608. return $this->precision ?: self::DEFAULT_PRECISION;
  1609. }
  1610. public function getScale(): int
  1611. {
  1612. return $this->scale ?: self::DEFAULT_SCALE;
  1613. }
  1614. public function hasLength(): bool
  1615. {
  1616. return in_array($this->type, ['varchar', 'varbinary']);
  1617. }
  1618. public function hasPrecision(): bool
  1619. {
  1620. return $this->type == 'decimal';
  1621. }
  1622. public function hasScale(): bool
  1623. {
  1624. return $this->type == 'decimal';
  1625. }
  1626. public function isBinary(): bool
  1627. {
  1628. return in_array($this->type, ['blob', 'varbinary']);
  1629. }
  1630. public function isBoolean(): bool
  1631. {
  1632. return $this->type == 'boolean';
  1633. }
  1634. public function isGeometry(): bool
  1635. {
  1636. return $this->type == 'geometry';
  1637. }
  1638. public function isInteger(): bool
  1639. {
  1640. return in_array($this->type, ['integer', 'bigint', 'smallint', 'tinyint']);
  1641. }
  1642. public function setPk($value) /*: void*/
  1643. {
  1644. $this->pk = $value;
  1645. }
  1646. public function getPk(): bool
  1647. {
  1648. return $this->pk;
  1649. }
  1650. public function setFk($value) /*: void*/
  1651. {
  1652. $this->fk = $value;
  1653. }
  1654. public function getFk(): string
  1655. {
  1656. return $this->fk;
  1657. }
  1658. public function serialize()
  1659. {
  1660. return [
  1661. 'name' => $this->name,
  1662. 'type' => $this->type,
  1663. 'length' => $this->length,
  1664. 'precision' => $this->precision,
  1665. 'scale' => $this->scale,
  1666. 'nullable' => $this->nullable,
  1667. 'pk' => $this->pk,
  1668. 'fk' => $this->fk,
  1669. ];
  1670. }
  1671. public function jsonSerialize()
  1672. {
  1673. return array_filter($this->serialize());
  1674. }
  1675. }
  1676. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedDatabase.php
  1677. class ReflectedDatabase implements \JsonSerializable
  1678. {
  1679. private $tableTypes;
  1680. public function __construct(array $tableTypes)
  1681. {
  1682. $this->tableTypes = $tableTypes;
  1683. }
  1684. public static function fromReflection(GenericReflection $reflection): ReflectedDatabase
  1685. {
  1686. $tableTypes = [];
  1687. foreach ($reflection->getTables() as $table) {
  1688. $tableName = $table['TABLE_NAME'];
  1689. $tableType = $table['TABLE_TYPE'];
  1690. if (in_array($tableName, $reflection->getIgnoredTables())) {
  1691. continue;
  1692. }
  1693. $tableTypes[$tableName] = $tableType;
  1694. }
  1695. return new ReflectedDatabase($tableTypes);
  1696. }
  1697. public static function fromJson( /* object */$json): ReflectedDatabase
  1698. {
  1699. $tableTypes = (array) $json->tables;
  1700. return new ReflectedDatabase($tableTypes);
  1701. }
  1702. public function hasTable(string $tableName): bool
  1703. {
  1704. return isset($this->tableTypes[$tableName]);
  1705. }
  1706. public function getType(string $tableName): string
  1707. {
  1708. return isset($this->tableTypes[$tableName]) ? $this->tableTypes[$tableName] : '';
  1709. }
  1710. public function getTableNames(): array
  1711. {
  1712. return array_keys($this->tableTypes);
  1713. }
  1714. public function removeTable(string $tableName): bool
  1715. {
  1716. if (!isset($this->tableTypes[$tableName])) {
  1717. return false;
  1718. }
  1719. unset($this->tableTypes[$tableName]);
  1720. return true;
  1721. }
  1722. public function serialize()
  1723. {
  1724. return [
  1725. 'tables' => $this->tableTypes,
  1726. ];
  1727. }
  1728. public function jsonSerialize()
  1729. {
  1730. return $this->serialize();
  1731. }
  1732. }
  1733. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedTable.php
  1734. class ReflectedTable implements \JsonSerializable
  1735. {
  1736. private $name;
  1737. private $type;
  1738. private $columns;
  1739. private $pk;
  1740. private $fks;
  1741. public function __construct(string $name, string $type, array $columns)
  1742. {
  1743. $this->name = $name;
  1744. $this->type = $type;
  1745. $this->columns = [];
  1746. foreach ($columns as $column) {
  1747. $columnName = $column->getName();
  1748. $this->columns[$columnName] = $column;
  1749. }
  1750. $this->pk = null;
  1751. foreach ($columns as $column) {
  1752. if ($column->getPk() == true) {
  1753. $this->pk = $column;
  1754. }
  1755. }
  1756. $this->fks = [];
  1757. foreach ($columns as $column) {
  1758. $columnName = $column->getName();
  1759. $referencedTableName = $column->getFk();
  1760. if ($referencedTableName != '') {
  1761. $this->fks[$columnName] = $referencedTableName;
  1762. }
  1763. }
  1764. }
  1765. public static function fromReflection(GenericReflection $reflection, string $name, string $type): ReflectedTable
  1766. {
  1767. $columns = [];
  1768. foreach ($reflection->getTableColumns($name, $type) as $tableColumn) {
  1769. $column = ReflectedColumn::fromReflection($reflection, $tableColumn);
  1770. $columns[$column->getName()] = $column;
  1771. }
  1772. $columnNames = $reflection->getTablePrimaryKeys($name);
  1773. if (count($columnNames) == 1) {
  1774. $columnName = $columnNames[0];
  1775. if (isset($columns[$columnName])) {
  1776. $pk = $columns[$columnName];
  1777. $pk->setPk(true);
  1778. }
  1779. }
  1780. $fks = $reflection->getTableForeignKeys($name);
  1781. foreach ($fks as $columnName => $table) {
  1782. $columns[$columnName]->setFk($table);
  1783. }
  1784. return new ReflectedTable($name, $type, array_values($columns));
  1785. }
  1786. public static function fromJson( /* object */$json): ReflectedTable
  1787. {
  1788. $name = $json->name;
  1789. $type = $json->type;
  1790. $columns = [];
  1791. if (isset($json->columns) && is_array($json->columns)) {
  1792. foreach ($json->columns as $column) {
  1793. $columns[] = ReflectedColumn::fromJson($column);
  1794. }
  1795. }
  1796. return new ReflectedTable($name, $type, $columns);
  1797. }
  1798. public function hasColumn(string $columnName): bool
  1799. {
  1800. return isset($this->columns[$columnName]);
  1801. }
  1802. public function hasPk(): bool
  1803. {
  1804. return $this->pk != null;
  1805. }
  1806. public function getPk() /*: ?ReflectedColumn */
  1807. {
  1808. return $this->pk;
  1809. }
  1810. public function getName(): string
  1811. {
  1812. return $this->name;
  1813. }
  1814. public function getType(): string
  1815. {
  1816. return $this->type;
  1817. }
  1818. public function getColumnNames(): array
  1819. {
  1820. return array_keys($this->columns);
  1821. }
  1822. public function getColumn($columnName): ReflectedColumn
  1823. {
  1824. return $this->columns[$columnName];
  1825. }
  1826. public function getFksTo(string $tableName): array
  1827. {
  1828. $columns = array();
  1829. foreach ($this->fks as $columnName => $referencedTableName) {
  1830. if ($tableName == $referencedTableName) {
  1831. $columns[] = $this->columns[$columnName];
  1832. }
  1833. }
  1834. return $columns;
  1835. }
  1836. public function removeColumn(string $columnName): bool
  1837. {
  1838. if (!isset($this->columns[$columnName])) {
  1839. return false;
  1840. }
  1841. unset($this->columns[$columnName]);
  1842. return true;
  1843. }
  1844. public function serialize()
  1845. {
  1846. return [
  1847. 'name' => $this->name,
  1848. 'type' => $this->type,
  1849. 'columns' => array_values($this->columns),
  1850. ];
  1851. }
  1852. public function jsonSerialize()
  1853. {
  1854. return $this->serialize();
  1855. }
  1856. }
  1857. // file: src/Tqdev/PhpCrudApi/Column/DefinitionService.php
  1858. class DefinitionService
  1859. {
  1860. private $db;
  1861. private $reflection;
  1862. public function __construct(GenericDB $db, ReflectionService $reflection)
  1863. {
  1864. $this->db = $db;
  1865. $this->reflection = $reflection;
  1866. }
  1867. public function updateTable(string $tableName, /* object */ $changes): bool
  1868. {
  1869. $table = $this->reflection->getTable($tableName);
  1870. $newTable = ReflectedTable::fromJson((object) array_merge((array) $table->jsonSerialize(), (array) $changes));
  1871. if ($table->getName() != $newTable->getName()) {
  1872. if (!$this->db->definition()->renameTable($table->getName(), $newTable->getName())) {
  1873. return false;
  1874. }
  1875. }
  1876. return true;
  1877. }
  1878. public function updateColumn(string $tableName, string $columnName, /* object */ $changes): bool
  1879. {
  1880. $table = $this->reflection->getTable($tableName);
  1881. $column = $table->getColumn($columnName);
  1882. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  1883. if ($newColumn->getPk() != $column->getPk() && $table->hasPk()) {
  1884. $oldColumn = $table->getPk();
  1885. if ($oldColumn->getName() != $columnName) {
  1886. $oldColumn->setPk(false);
  1887. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $oldColumn->getName(), $oldColumn)) {
  1888. return false;
  1889. }
  1890. }
  1891. }
  1892. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), ['pk' => false, 'fk' => false]));
  1893. if ($newColumn->getPk() != $column->getPk() && !$newColumn->getPk()) {
  1894. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $column->getName(), $newColumn)) {
  1895. return false;
  1896. }
  1897. }
  1898. if ($newColumn->getFk() != $column->getFk() && !$newColumn->getFk()) {
  1899. if (!$this->db->definition()->removeColumnForeignKey($table->getName(), $column->getName(), $newColumn)) {
  1900. return false;
  1901. }
  1902. }
  1903. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  1904. $newColumn->setPk(false);
  1905. $newColumn->setFk('');
  1906. if ($newColumn->getName() != $column->getName()) {
  1907. if (!$this->db->definition()->renameColumn($table->getName(), $column->getName(), $newColumn)) {
  1908. return false;
  1909. }
  1910. }
  1911. if ($newColumn->getType() != $column->getType() ||
  1912. $newColumn->getLength() != $column->getLength() ||
  1913. $newColumn->getPrecision() != $column->getPrecision() ||
  1914. $newColumn->getScale() != $column->getScale()
  1915. ) {
  1916. if (!$this->db->definition()->retypeColumn($table->getName(), $newColumn->getName(), $newColumn)) {
  1917. return false;
  1918. }
  1919. }
  1920. if ($newColumn->getNullable() != $column->getNullable()) {
  1921. if (!$this->db->definition()->setColumnNullable($table->getName(), $newColumn->getName(), $newColumn)) {
  1922. return false;
  1923. }
  1924. }
  1925. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  1926. if ($newColumn->getFk()) {
  1927. if (!$this->db->definition()->addColumnForeignKey($table->getName(), $newColumn->getName(), $newColumn)) {
  1928. return false;
  1929. }
  1930. }
  1931. if ($newColumn->getPk()) {
  1932. if (!$this->db->definition()->addColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
  1933. return false;
  1934. }
  1935. }
  1936. return true;
  1937. }
  1938. public function addTable( /* object */$definition)
  1939. {
  1940. $newTable = ReflectedTable::fromJson($definition);
  1941. if (!$this->db->definition()->addTable($newTable)) {
  1942. return false;
  1943. }
  1944. return true;
  1945. }
  1946. public function addColumn(string $tableName, /* object */ $definition)
  1947. {
  1948. $newColumn = ReflectedColumn::fromJson($definition);
  1949. if (!$this->db->definition()->addColumn($tableName, $newColumn)) {
  1950. return false;
  1951. }
  1952. if ($newColumn->getFk()) {
  1953. if (!$this->db->definition()->addColumnForeignKey($tableName, $newColumn->getName(), $newColumn)) {
  1954. return false;
  1955. }
  1956. }
  1957. if ($newColumn->getPk()) {
  1958. if (!$this->db->definition()->addColumnPrimaryKey($tableName, $newColumn->getName(), $newColumn)) {
  1959. return false;
  1960. }
  1961. }
  1962. return true;
  1963. }
  1964. public function removeTable(string $tableName)
  1965. {
  1966. if (!$this->db->definition()->removeTable($tableName)) {
  1967. return false;
  1968. }
  1969. return true;
  1970. }
  1971. public function removeColumn(string $tableName, string $columnName)
  1972. {
  1973. $table = $this->reflection->getTable($tableName);
  1974. $newColumn = $table->getColumn($columnName);
  1975. if ($newColumn->getPk()) {
  1976. $newColumn->setPk(false);
  1977. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
  1978. return false;
  1979. }
  1980. }
  1981. if ($newColumn->getFk()) {
  1982. $newColumn->setFk("");
  1983. if (!$this->db->definition()->removeColumnForeignKey($tableName, $columnName, $newColumn)) {
  1984. return false;
  1985. }
  1986. }
  1987. if (!$this->db->definition()->removeColumn($tableName, $columnName)) {
  1988. return false;
  1989. }
  1990. return true;
  1991. }
  1992. }
  1993. // file: src/Tqdev/PhpCrudApi/Column/ReflectionService.php
  1994. class ReflectionService
  1995. {
  1996. private $db;
  1997. private $cache;
  1998. private $ttl;
  1999. private $database;
  2000. private $tables;
  2001. public function __construct(GenericDB $db, Cache $cache, int $ttl)
  2002. {
  2003. $this->db = $db;
  2004. $this->cache = $cache;
  2005. $this->ttl = $ttl;
  2006. $this->database = $this->loadDatabase(true);
  2007. $this->tables = [];
  2008. }
  2009. private function loadDatabase(bool $useCache): ReflectedDatabase
  2010. {
  2011. $data = $useCache ? $this->cache->get('ReflectedDatabase') : '';
  2012. if ($data != '') {
  2013. $database = ReflectedDatabase::fromJson(json_decode(gzuncompress($data)));
  2014. } else {
  2015. $database = ReflectedDatabase::fromReflection($this->db->reflection());
  2016. $data = gzcompress(json_encode($database, JSON_UNESCAPED_UNICODE));
  2017. $this->cache->set('ReflectedDatabase', $data, $this->ttl);
  2018. }
  2019. return $database;
  2020. }
  2021. private function loadTable(string $tableName, bool $useCache): ReflectedTable
  2022. {
  2023. $data = $useCache ? $this->cache->get("ReflectedTable($tableName)") : '';
  2024. if ($data != '') {
  2025. $table = ReflectedTable::fromJson(json_decode(gzuncompress($data)));
  2026. } else {
  2027. $tableType = $this->database->getType($tableName);
  2028. $table = ReflectedTable::fromReflection($this->db->reflection(), $tableName, $tableType);
  2029. $data = gzcompress(json_encode($table, JSON_UNESCAPED_UNICODE));
  2030. $this->cache->set("ReflectedTable($tableName)", $data, $this->ttl);
  2031. }
  2032. return $table;
  2033. }
  2034. public function refreshTables()
  2035. {
  2036. $this->database = $this->loadDatabase(false);
  2037. }
  2038. public function refreshTable(string $tableName)
  2039. {
  2040. $this->tables[$tableName] = $this->loadTable($tableName, false);
  2041. }
  2042. public function hasTable(string $tableName): bool
  2043. {
  2044. return $this->database->hasTable($tableName);
  2045. }
  2046. public function getType(string $tableName): string
  2047. {
  2048. return $this->database->getType($tableName);
  2049. }
  2050. public function getTable(string $tableName): ReflectedTable
  2051. {
  2052. if (!isset($this->tables[$tableName])) {
  2053. $this->tables[$tableName] = $this->loadTable($tableName, true);
  2054. }
  2055. return $this->tables[$tableName];
  2056. }
  2057. public function getTableNames(): array
  2058. {
  2059. return $this->database->getTableNames();
  2060. }
  2061. public function getDatabaseName(): string
  2062. {
  2063. return $this->database->getName();
  2064. }
  2065. public function removeTable(string $tableName): bool
  2066. {
  2067. unset($this->tables[$tableName]);
  2068. return $this->database->removeTable($tableName);
  2069. }
  2070. }
  2071. // file: src/Tqdev/PhpCrudApi/Controller/CacheController.php
  2072. class CacheController
  2073. {
  2074. private $cache;
  2075. private $responder;
  2076. public function __construct(Router $router, Responder $responder, Cache $cache)
  2077. {
  2078. $router->register('GET', '/cache/clear', array($this, 'clear'));
  2079. $this->cache = $cache;
  2080. $this->responder = $responder;
  2081. }
  2082. public function clear(ServerRequestInterface $request): ResponseInterface
  2083. {
  2084. return $this->responder->success($this->cache->clear());
  2085. }
  2086. }
  2087. // file: src/Tqdev/PhpCrudApi/Controller/ColumnController.php
  2088. class ColumnController
  2089. {
  2090. private $responder;
  2091. private $reflection;
  2092. private $definition;
  2093. public function __construct(Router $router, Responder $responder, ReflectionService $reflection, DefinitionService $definition)
  2094. {
  2095. $router->register('GET', '/columns', array($this, 'getDatabase'));
  2096. $router->register('GET', '/columns/*', array($this, 'getTable'));
  2097. $router->register('GET', '/columns/*/*', array($this, 'getColumn'));
  2098. $router->register('PUT', '/columns/*', array($this, 'updateTable'));
  2099. $router->register('PUT', '/columns/*/*', array($this, 'updateColumn'));
  2100. $router->register('POST', '/columns', array($this, 'addTable'));
  2101. $router->register('POST', '/columns/*', array($this, 'addColumn'));
  2102. $router->register('DELETE', '/columns/*', array($this, 'removeTable'));
  2103. $router->register('DELETE', '/columns/*/*', array($this, 'removeColumn'));
  2104. $this->responder = $responder;
  2105. $this->reflection = $reflection;
  2106. $this->definition = $definition;
  2107. }
  2108. public function getDatabase(ServerRequestInterface $request): ResponseInterface
  2109. {
  2110. $tables = [];
  2111. foreach ($this->reflection->getTableNames() as $table) {
  2112. $tables[] = $this->reflection->getTable($table);
  2113. }
  2114. $database = ['tables' => $tables];
  2115. return $this->responder->success($database);
  2116. }
  2117. public function getTable(ServerRequestInterface $request): ResponseInterface
  2118. {
  2119. $tableName = RequestUtils::getPathSegment($request, 2);
  2120. if (!$this->reflection->hasTable($tableName)) {
  2121. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2122. }
  2123. $table = $this->reflection->getTable($tableName);
  2124. return $this->responder->success($table);
  2125. }
  2126. public function getColumn(ServerRequestInterface $request): ResponseInterface
  2127. {
  2128. $tableName = RequestUtils::getPathSegment($request, 2);
  2129. $columnName = RequestUtils::getPathSegment($request, 3);
  2130. if (!$this->reflection->hasTable($tableName)) {
  2131. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2132. }
  2133. $table = $this->reflection->getTable($tableName);
  2134. if (!$table->hasColumn($columnName)) {
  2135. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  2136. }
  2137. $column = $table->getColumn($columnName);
  2138. return $this->responder->success($column);
  2139. }
  2140. public function updateTable(ServerRequestInterface $request): ResponseInterface
  2141. {
  2142. $tableName = RequestUtils::getPathSegment($request, 2);
  2143. if (!$this->reflection->hasTable($tableName)) {
  2144. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2145. }
  2146. $success = $this->definition->updateTable($tableName, $request->getParsedBody());
  2147. if ($success) {
  2148. $this->reflection->refreshTables();
  2149. }
  2150. return $this->responder->success($success);
  2151. }
  2152. public function updateColumn(ServerRequestInterface $request): ResponseInterface
  2153. {
  2154. $tableName = RequestUtils::getPathSegment($request, 2);
  2155. $columnName = RequestUtils::getPathSegment($request, 3);
  2156. if (!$this->reflection->hasTable($tableName)) {
  2157. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2158. }
  2159. $table = $this->reflection->getTable($tableName);
  2160. if (!$table->hasColumn($columnName)) {
  2161. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  2162. }
  2163. $success = $this->definition->updateColumn($tableName, $columnName, $request->getParsedBody());
  2164. if ($success) {
  2165. $this->reflection->refreshTable($tableName);
  2166. }
  2167. return $this->responder->success($success);
  2168. }
  2169. public function addTable(ServerRequestInterface $request): ResponseInterface
  2170. {
  2171. $tableName = $request->getParsedBody()->name;
  2172. if ($this->reflection->hasTable($tableName)) {
  2173. return $this->responder->error(ErrorCode::TABLE_ALREADY_EXISTS, $tableName);
  2174. }
  2175. $success = $this->definition->addTable($request->getParsedBody());
  2176. if ($success) {
  2177. $this->reflection->refreshTables();
  2178. }
  2179. return $this->responder->success($success);
  2180. }
  2181. public function addColumn(ServerRequestInterface $request): ResponseInterface
  2182. {
  2183. $tableName = RequestUtils::getPathSegment($request, 2);
  2184. if (!$this->reflection->hasTable($tableName)) {
  2185. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2186. }
  2187. $columnName = $request->getParsedBody()->name;
  2188. $table = $this->reflection->getTable($tableName);
  2189. if ($table->hasColumn($columnName)) {
  2190. return $this->responder->error(ErrorCode::COLUMN_ALREADY_EXISTS, $columnName);
  2191. }
  2192. $success = $this->definition->addColumn($tableName, $request->getParsedBody());
  2193. if ($success) {
  2194. $this->reflection->refreshTable($tableName);
  2195. }
  2196. return $this->responder->success($success);
  2197. }
  2198. public function removeTable(ServerRequestInterface $request): ResponseInterface
  2199. {
  2200. $tableName = RequestUtils::getPathSegment($request, 2);
  2201. if (!$this->reflection->hasTable($tableName)) {
  2202. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2203. }
  2204. $success = $this->definition->removeTable($tableName);
  2205. if ($success) {
  2206. $this->reflection->refreshTables();
  2207. }
  2208. return $this->responder->success($success);
  2209. }
  2210. public function removeColumn(ServerRequestInterface $request): ResponseInterface
  2211. {
  2212. $tableName = RequestUtils::getPathSegment($request, 2);
  2213. $columnName = RequestUtils::getPathSegment($request, 3);
  2214. if (!$this->reflection->hasTable($tableName)) {
  2215. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2216. }
  2217. $table = $this->reflection->getTable($tableName);
  2218. if (!$table->hasColumn($columnName)) {
  2219. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  2220. }
  2221. $success = $this->definition->removeColumn($tableName, $columnName);
  2222. if ($success) {
  2223. $this->reflection->refreshTable($tableName);
  2224. }
  2225. return $this->responder->success($success);
  2226. }
  2227. }
  2228. // file: src/Tqdev/PhpCrudApi/Controller/OpenApiController.php
  2229. class OpenApiController
  2230. {
  2231. private $openApi;
  2232. private $responder;
  2233. public function __construct(Router $router, Responder $responder, OpenApiService $openApi)
  2234. {
  2235. $router->register('GET', '/openapi', array($this, 'openapi'));
  2236. $this->openApi = $openApi;
  2237. $this->responder = $responder;
  2238. }
  2239. public function openapi(ServerRequestInterface $request): ResponseInterface
  2240. {
  2241. return $this->responder->success($this->openApi->get());
  2242. }
  2243. }
  2244. // file: src/Tqdev/PhpCrudApi/Controller/RecordController.php
  2245. class RecordController
  2246. {
  2247. private $service;
  2248. private $responder;
  2249. public function __construct(Router $router, Responder $responder, RecordService $service)
  2250. {
  2251. $router->register('GET', '/records/*', array($this, '_list'));
  2252. $router->register('POST', '/records/*', array($this, 'create'));
  2253. $router->register('GET', '/records/*/*', array($this, 'read'));
  2254. $router->register('PUT', '/records/*/*', array($this, 'update'));
  2255. $router->register('DELETE', '/records/*/*', array($this, 'delete'));
  2256. $router->register('PATCH', '/records/*/*', array($this, 'increment'));
  2257. $this->service = $service;
  2258. $this->responder = $responder;
  2259. }
  2260. public function _list(ServerRequestInterface $request): ResponseInterface
  2261. {
  2262. $table = RequestUtils::getPathSegment($request, 2);
  2263. $params = RequestUtils::getParams($request);
  2264. if (!$this->service->hasTable($table)) {
  2265. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2266. }
  2267. return $this->responder->success($this->service->_list($table, $params));
  2268. }
  2269. public function read(ServerRequestInterface $request): ResponseInterface
  2270. {
  2271. $table = RequestUtils::getPathSegment($request, 2);
  2272. if (!$this->service->hasTable($table)) {
  2273. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2274. }
  2275. if ($this->service->getType($table) != 'table') {
  2276. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2277. }
  2278. $id = RequestUtils::getPathSegment($request, 3);
  2279. $params = RequestUtils::getParams($request);
  2280. if (strpos($id, ',') !== false) {
  2281. $ids = explode(',', $id);
  2282. $result = [];
  2283. for ($i = 0; $i < count($ids); $i++) {
  2284. array_push($result, $this->service->read($table, $ids[$i], $params));
  2285. }
  2286. return $this->responder->success($result);
  2287. } else {
  2288. $response = $this->service->read($table, $id, $params);
  2289. if ($response === null) {
  2290. return $this->responder->error(ErrorCode::RECORD_NOT_FOUND, $id);
  2291. }
  2292. return $this->responder->success($response);
  2293. }
  2294. }
  2295. public function create(ServerRequestInterface $request): ResponseInterface
  2296. {
  2297. $table = RequestUtils::getPathSegment($request, 2);
  2298. if (!$this->service->hasTable($table)) {
  2299. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2300. }
  2301. if ($this->service->getType($table) != 'table') {
  2302. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2303. }
  2304. $record = $request->getParsedBody();
  2305. if ($record === null) {
  2306. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  2307. }
  2308. $params = RequestUtils::getParams($request);
  2309. if (is_array($record)) {
  2310. $result = array();
  2311. foreach ($record as $r) {
  2312. $result[] = $this->service->create($table, $r, $params);
  2313. }
  2314. return $this->responder->success($result);
  2315. } else {
  2316. return $this->responder->success($this->service->create($table, $record, $params));
  2317. }
  2318. }
  2319. public function update(ServerRequestInterface $request): ResponseInterface
  2320. {
  2321. $table = RequestUtils::getPathSegment($request, 2);
  2322. if (!$this->service->hasTable($table)) {
  2323. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2324. }
  2325. if ($this->service->getType($table) != 'table') {
  2326. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2327. }
  2328. $id = RequestUtils::getPathSegment($request, 3);
  2329. $params = RequestUtils::getParams($request);
  2330. $record = $request->getParsedBody();
  2331. if ($record === null) {
  2332. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  2333. }
  2334. $ids = explode(',', $id);
  2335. if (is_array($record)) {
  2336. if (count($ids) != count($record)) {
  2337. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2338. }
  2339. $result = array();
  2340. for ($i = 0; $i < count($ids); $i++) {
  2341. $result[] = $this->service->update($table, $ids[$i], $record[$i], $params);
  2342. }
  2343. return $this->responder->success($result);
  2344. } else {
  2345. if (count($ids) != 1) {
  2346. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2347. }
  2348. return $this->responder->success($this->service->update($table, $id, $record, $params));
  2349. }
  2350. }
  2351. public function delete(ServerRequestInterface $request): ResponseInterface
  2352. {
  2353. $table = RequestUtils::getPathSegment($request, 2);
  2354. if (!$this->service->hasTable($table)) {
  2355. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2356. }
  2357. if ($this->service->getType($table) != 'table') {
  2358. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2359. }
  2360. $id = RequestUtils::getPathSegment($request, 3);
  2361. $params = RequestUtils::getParams($request);
  2362. $ids = explode(',', $id);
  2363. if (count($ids) > 1) {
  2364. $result = array();
  2365. for ($i = 0; $i < count($ids); $i++) {
  2366. $result[] = $this->service->delete($table, $ids[$i], $params);
  2367. }
  2368. return $this->responder->success($result);
  2369. } else {
  2370. return $this->responder->success($this->service->delete($table, $id, $params));
  2371. }
  2372. }
  2373. public function increment(ServerRequestInterface $request): ResponseInterface
  2374. {
  2375. $table = RequestUtils::getPathSegment($request, 2);
  2376. if (!$this->service->hasTable($table)) {
  2377. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2378. }
  2379. if ($this->service->getType($table) != 'table') {
  2380. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2381. }
  2382. $id = RequestUtils::getPathSegment($request, 3);
  2383. $record = $request->getParsedBody();
  2384. if ($record === null) {
  2385. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  2386. }
  2387. $params = RequestUtils::getParams($request);
  2388. $ids = explode(',', $id);
  2389. if (is_array($record)) {
  2390. if (count($ids) != count($record)) {
  2391. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2392. }
  2393. $result = array();
  2394. for ($i = 0; $i < count($ids); $i++) {
  2395. $result[] = $this->service->increment($table, $ids[$i], $record[$i], $params);
  2396. }
  2397. return $this->responder->success($result);
  2398. } else {
  2399. if (count($ids) != 1) {
  2400. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2401. }
  2402. return $this->responder->success($this->service->increment($table, $id, $record, $params));
  2403. }
  2404. }
  2405. }
  2406. // file: src/Tqdev/PhpCrudApi/Controller/Responder.php
  2407. class Responder
  2408. {
  2409. public function error(int $error, string $argument, $details = null): ResponseInterface
  2410. {
  2411. $errorCode = new ErrorCode($error);
  2412. $status = $errorCode->getStatus();
  2413. $document = new ErrorDocument($errorCode, $argument, $details);
  2414. return ResponseFactory::fromObject($status, $document);
  2415. }
  2416. public function success($result): ResponseInterface
  2417. {
  2418. return ResponseFactory::fromObject(ResponseFactory::OK, $result);
  2419. }
  2420. }
  2421. // file: src/Tqdev/PhpCrudApi/Database/ColumnConverter.php
  2422. class ColumnConverter
  2423. {
  2424. private $driver;
  2425. public function __construct(string $driver)
  2426. {
  2427. $this->driver = $driver;
  2428. }
  2429. public function convertColumnValue(ReflectedColumn $column): string
  2430. {
  2431. if ($column->isBinary()) {
  2432. switch ($this->driver) {
  2433. case 'mysql':
  2434. return "FROM_BASE64(?)";
  2435. case 'pgsql':
  2436. return "decode(?, 'base64')";
  2437. case 'sqlsrv':
  2438. return "CONVERT(XML, ?).value('.','varbinary(max)')";
  2439. }
  2440. }
  2441. if ($column->isGeometry()) {
  2442. switch ($this->driver) {
  2443. case 'mysql':
  2444. case 'pgsql':
  2445. return "ST_GeomFromText(?)";
  2446. case 'sqlsrv':
  2447. return "geometry::STGeomFromText(?,0)";
  2448. }
  2449. }
  2450. return '?';
  2451. }
  2452. public function convertColumnName(ReflectedColumn $column, $value): string
  2453. {
  2454. if ($column->isBinary()) {
  2455. switch ($this->driver) {
  2456. case 'mysql':
  2457. return "TO_BASE64($value) as $value";
  2458. case 'pgsql':
  2459. return "encode($value::bytea, 'base64') as $value";
  2460. case 'sqlsrv':
  2461. return "CASE WHEN $value IS NULL THEN NULL ELSE (SELECT CAST($value as varbinary(max)) FOR XML PATH(''), BINARY BASE64) END as $value";
  2462. }
  2463. }
  2464. if ($column->isGeometry()) {
  2465. switch ($this->driver) {
  2466. case 'mysql':
  2467. case 'pgsql':
  2468. return "ST_AsText($value) as $value";
  2469. case 'sqlsrv':
  2470. return "REPLACE($value.STAsText(),' (','(') as $value";
  2471. }
  2472. }
  2473. return $value;
  2474. }
  2475. }
  2476. // file: src/Tqdev/PhpCrudApi/Database/ColumnsBuilder.php
  2477. class ColumnsBuilder
  2478. {
  2479. private $driver;
  2480. private $converter;
  2481. public function __construct(string $driver)
  2482. {
  2483. $this->driver = $driver;
  2484. $this->converter = new ColumnConverter($driver);
  2485. }
  2486. public function getOffsetLimit(int $offset, int $limit): string
  2487. {
  2488. if ($limit < 0 || $offset < 0) {
  2489. return '';
  2490. }
  2491. switch ($this->driver) {
  2492. case 'mysql':return " LIMIT $offset, $limit";
  2493. case 'pgsql':return " LIMIT $limit OFFSET $offset";
  2494. case 'sqlsrv':return " OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY";
  2495. }
  2496. }
  2497. private function quoteColumnName(ReflectedColumn $column): string
  2498. {
  2499. return '"' . $column->getName() . '"';
  2500. }
  2501. public function getOrderBy(ReflectedTable $table, array $columnOrdering): string
  2502. {
  2503. if (count($columnOrdering) == 0) {
  2504. return '';
  2505. }
  2506. $results = array();
  2507. foreach ($columnOrdering as $i => list($columnName, $ordering)) {
  2508. $column = $table->getColumn($columnName);
  2509. $quotedColumnName = $this->quoteColumnName($column);
  2510. $results[] = $quotedColumnName . ' ' . $ordering;
  2511. }
  2512. return ' ORDER BY ' . implode(',', $results);
  2513. }
  2514. public function getSelect(ReflectedTable $table, array $columnNames): string
  2515. {
  2516. $results = array();
  2517. foreach ($columnNames as $columnName) {
  2518. $column = $table->getColumn($columnName);
  2519. $quotedColumnName = $this->quoteColumnName($column);
  2520. $quotedColumnName = $this->converter->convertColumnName($column, $quotedColumnName);
  2521. $results[] = $quotedColumnName;
  2522. }
  2523. return implode(',', $results);
  2524. }
  2525. public function getInsert(ReflectedTable $table, array $columnValues): string
  2526. {
  2527. $columns = array();
  2528. $values = array();
  2529. foreach ($columnValues as $columnName => $columnValue) {
  2530. $column = $table->getColumn($columnName);
  2531. $quotedColumnName = $this->quoteColumnName($column);
  2532. $columns[] = $quotedColumnName;
  2533. $columnValue = $this->converter->convertColumnValue($column);
  2534. $values[] = $columnValue;
  2535. }
  2536. $columnsSql = '(' . implode(',', $columns) . ')';
  2537. $valuesSql = '(' . implode(',', $values) . ')';
  2538. $outputColumn = $this->quoteColumnName($table->getPk());
  2539. switch ($this->driver) {
  2540. case 'mysql':return "$columnsSql VALUES $valuesSql";
  2541. case 'pgsql':return "$columnsSql VALUES $valuesSql RETURNING $outputColumn";
  2542. case 'sqlsrv':return "$columnsSql OUTPUT INSERTED.$outputColumn VALUES $valuesSql";
  2543. }
  2544. }
  2545. public function getUpdate(ReflectedTable $table, array $columnValues): string
  2546. {
  2547. $results = array();
  2548. foreach ($columnValues as $columnName => $columnValue) {
  2549. $column = $table->getColumn($columnName);
  2550. $quotedColumnName = $this->quoteColumnName($column);
  2551. $columnValue = $this->converter->convertColumnValue($column);
  2552. $results[] = $quotedColumnName . '=' . $columnValue;
  2553. }
  2554. return implode(',', $results);
  2555. }
  2556. public function getIncrement(ReflectedTable $table, array $columnValues): string
  2557. {
  2558. $results = array();
  2559. foreach ($columnValues as $columnName => $columnValue) {
  2560. if (!is_numeric($columnValue)) {
  2561. continue;
  2562. }
  2563. $column = $table->getColumn($columnName);
  2564. $quotedColumnName = $this->quoteColumnName($column);
  2565. $columnValue = $this->converter->convertColumnValue($column);
  2566. $results[] = $quotedColumnName . '=' . $quotedColumnName . '+' . $columnValue;
  2567. }
  2568. return implode(',', $results);
  2569. }
  2570. }
  2571. // file: src/Tqdev/PhpCrudApi/Database/ConditionsBuilder.php
  2572. class ConditionsBuilder
  2573. {
  2574. private $driver;
  2575. public function __construct(string $driver)
  2576. {
  2577. $this->driver = $driver;
  2578. }
  2579. private function getConditionSql(Condition $condition, array &$arguments): string
  2580. {
  2581. if ($condition instanceof AndCondition) {
  2582. return $this->getAndConditionSql($condition, $arguments);
  2583. }
  2584. if ($condition instanceof OrCondition) {
  2585. return $this->getOrConditionSql($condition, $arguments);
  2586. }
  2587. if ($condition instanceof NotCondition) {
  2588. return $this->getNotConditionSql($condition, $arguments);
  2589. }
  2590. if ($condition instanceof SpatialCondition) {
  2591. return $this->getSpatialConditionSql($condition, $arguments);
  2592. }
  2593. if ($condition instanceof ColumnCondition) {
  2594. return $this->getColumnConditionSql($condition, $arguments);
  2595. }
  2596. throw new \Exception('Unknown Condition: ' . get_class($condition));
  2597. }
  2598. private function getAndConditionSql(AndCondition $and, array &$arguments): string
  2599. {
  2600. $parts = [];
  2601. foreach ($and->getConditions() as $condition) {
  2602. $parts[] = $this->getConditionSql($condition, $arguments);
  2603. }
  2604. return '(' . implode(' AND ', $parts) . ')';
  2605. }
  2606. private function getOrConditionSql(OrCondition $or, array &$arguments): string
  2607. {
  2608. $parts = [];
  2609. foreach ($or->getConditions() as $condition) {
  2610. $parts[] = $this->getConditionSql($condition, $arguments);
  2611. }
  2612. return '(' . implode(' OR ', $parts) . ')';
  2613. }
  2614. private function getNotConditionSql(NotCondition $not, array &$arguments): string
  2615. {
  2616. $condition = $not->getCondition();
  2617. return '(NOT ' . $this->getConditionSql($condition, $arguments) . ')';
  2618. }
  2619. private function quoteColumnName(ReflectedColumn $column): string
  2620. {
  2621. return '"' . $column->getName() . '"';
  2622. }
  2623. private function escapeLikeValue(string $value): string
  2624. {
  2625. return addcslashes($value, '%_');
  2626. }
  2627. private function getColumnConditionSql(ColumnCondition $condition, array &$arguments): string
  2628. {
  2629. $column = $this->quoteColumnName($condition->getColumn());
  2630. $operator = $condition->getOperator();
  2631. $value = $condition->getValue();
  2632. switch ($operator) {
  2633. case 'cs':
  2634. $sql = "$column LIKE ?";
  2635. $arguments[] = '%' . $this->escapeLikeValue($value) . '%';
  2636. break;
  2637. case 'sw':
  2638. $sql = "$column LIKE ?";
  2639. $arguments[] = $this->escapeLikeValue($value) . '%';
  2640. break;
  2641. case 'ew':
  2642. $sql = "$column LIKE ?";
  2643. $arguments[] = '%' . $this->escapeLikeValue($value);
  2644. break;
  2645. case 'eq':
  2646. $sql = "$column = ?";
  2647. $arguments[] = $value;
  2648. break;
  2649. case 'lt':
  2650. $sql = "$column < ?";
  2651. $arguments[] = $value;
  2652. break;
  2653. case 'le':
  2654. $sql = "$column <= ?";
  2655. $arguments[] = $value;
  2656. break;
  2657. case 'ge':
  2658. $sql = "$column >= ?";
  2659. $arguments[] = $value;
  2660. break;
  2661. case 'gt':
  2662. $sql = "$column > ?";
  2663. $arguments[] = $value;
  2664. break;
  2665. case 'bt':
  2666. $parts = explode(',', $value, 2);
  2667. $count = count($parts);
  2668. if ($count == 2) {
  2669. $sql = "($column >= ? AND $column <= ?)";
  2670. $arguments[] = $parts[0];
  2671. $arguments[] = $parts[1];
  2672. } else {
  2673. $sql = "FALSE";
  2674. }
  2675. break;
  2676. case 'in':
  2677. $parts = explode(',', $value);
  2678. $count = count($parts);
  2679. if ($count > 0) {
  2680. $qmarks = implode(',', str_split(str_repeat('?', $count)));
  2681. $sql = "$column IN ($qmarks)";
  2682. for ($i = 0; $i < $count; $i++) {
  2683. $arguments[] = $parts[$i];
  2684. }
  2685. } else {
  2686. $sql = "FALSE";
  2687. }
  2688. break;
  2689. case 'is':
  2690. $sql = "$column IS NULL";
  2691. break;
  2692. }
  2693. return $sql;
  2694. }
  2695. private function getSpatialFunctionName(string $operator): string
  2696. {
  2697. switch ($operator) {
  2698. case 'co':return 'ST_Contains';
  2699. case 'cr':return 'ST_Crosses';
  2700. case 'di':return 'ST_Disjoint';
  2701. case 'eq':return 'ST_Equals';
  2702. case 'in':return 'ST_Intersects';
  2703. case 'ov':return 'ST_Overlaps';
  2704. case 'to':return 'ST_Touches';
  2705. case 'wi':return 'ST_Within';
  2706. case 'ic':return 'ST_IsClosed';
  2707. case 'is':return 'ST_IsSimple';
  2708. case 'iv':return 'ST_IsValid';
  2709. }
  2710. }
  2711. private function hasSpatialArgument(string $operator): bool
  2712. {
  2713. return in_array($operator, ['ic', 'is', 'iv']) ? false : true;
  2714. }
  2715. private function getSpatialFunctionCall(string $functionName, string $column, bool $hasArgument): string
  2716. {
  2717. switch ($this->driver) {
  2718. case 'mysql':
  2719. case 'pgsql':
  2720. $argument = $hasArgument ? 'ST_GeomFromText(?)' : '';
  2721. return "$functionName($column, $argument)=TRUE";
  2722. case 'sqlsrv':
  2723. $functionName = str_replace('ST_', 'ST', $functionName);
  2724. $argument = $hasArgument ? 'geometry::STGeomFromText(?,0)' : '';
  2725. return "$column.$functionName($argument)=1";
  2726. }
  2727. }
  2728. private function getSpatialConditionSql(ColumnCondition $condition, array &$arguments): string
  2729. {
  2730. $column = $this->quoteColumnName($condition->getColumn());
  2731. $operator = $condition->getOperator();
  2732. $value = $condition->getValue();
  2733. $functionName = $this->getSpatialFunctionName($operator);
  2734. $hasArgument = $this->hasSpatialArgument($operator);
  2735. $sql = $this->getSpatialFunctionCall($functionName, $column, $hasArgument);
  2736. if ($hasArgument) {
  2737. $arguments[] = $value;
  2738. }
  2739. return $sql;
  2740. }
  2741. public function getWhereClause(Condition $condition, array &$arguments): string
  2742. {
  2743. if ($condition instanceof NoCondition) {
  2744. return '';
  2745. }
  2746. return ' WHERE ' . $this->getConditionSql($condition, $arguments);
  2747. }
  2748. }
  2749. // file: src/Tqdev/PhpCrudApi/Database/DataConverter.php
  2750. class DataConverter
  2751. {
  2752. private $driver;
  2753. public function __construct(string $driver)
  2754. {
  2755. $this->driver = $driver;
  2756. }
  2757. private function convertRecordValue($conversion, $value)
  2758. {
  2759. switch ($conversion) {
  2760. case 'boolean':
  2761. return $value ? true : false;
  2762. case 'integer':
  2763. return (int) $value;
  2764. }
  2765. return $value;
  2766. }
  2767. private function getRecordValueConversion(ReflectedColumn $column): string
  2768. {
  2769. if (in_array($this->driver, ['mysql', 'sqlsrv']) && $column->isBoolean()) {
  2770. return 'boolean';
  2771. }
  2772. if ($this->driver == 'sqlsrv' && $column->getType() == 'bigint') {
  2773. return 'integer';
  2774. }
  2775. return 'none';
  2776. }
  2777. public function convertRecords(ReflectedTable $table, array $columnNames, array &$records) /*: void*/
  2778. {
  2779. foreach ($columnNames as $columnName) {
  2780. $column = $table->getColumn($columnName);
  2781. $conversion = $this->getRecordValueConversion($column);
  2782. if ($conversion != 'none') {
  2783. foreach ($records as $i => $record) {
  2784. $value = $records[$i][$columnName];
  2785. if ($value === null) {
  2786. continue;
  2787. }
  2788. $records[$i][$columnName] = $this->convertRecordValue($conversion, $value);
  2789. }
  2790. }
  2791. }
  2792. }
  2793. private function convertInputValue($conversion, $value)
  2794. {
  2795. switch ($conversion) {
  2796. case 'base64url_to_base64':
  2797. return str_pad(strtr($value, '-_', '+/'), ceil(strlen($value) / 4) * 4, '=', STR_PAD_RIGHT);
  2798. }
  2799. return $value;
  2800. }
  2801. private function getInputValueConversion(ReflectedColumn $column): string
  2802. {
  2803. if ($column->isBinary()) {
  2804. return 'base64url_to_base64';
  2805. }
  2806. return 'none';
  2807. }
  2808. public function convertColumnValues(ReflectedTable $table, array &$columnValues) /*: void*/
  2809. {
  2810. $columnNames = array_keys($columnValues);
  2811. foreach ($columnNames as $columnName) {
  2812. $column = $table->getColumn($columnName);
  2813. $conversion = $this->getInputValueConversion($column);
  2814. if ($conversion != 'none') {
  2815. $value = $columnValues[$columnName];
  2816. if ($value !== null) {
  2817. $columnValues[$columnName] = $this->convertInputValue($conversion, $value);
  2818. }
  2819. }
  2820. }
  2821. }
  2822. }
  2823. // file: src/Tqdev/PhpCrudApi/Database/GenericDB.php
  2824. class GenericDB
  2825. {
  2826. private $driver;
  2827. private $database;
  2828. private $pdo;
  2829. private $reflection;
  2830. private $definition;
  2831. private $conditions;
  2832. private $columns;
  2833. private $converter;
  2834. private function getDsn(string $address, string $port = null, string $database = null): string
  2835. {
  2836. switch ($this->driver) {
  2837. case 'mysql':return "$this->driver:host=$address;port=$port;dbname=$database;charset=utf8mb4";
  2838. case 'pgsql':return "$this->driver:host=$address port=$port dbname=$database options='--client_encoding=UTF8'";
  2839. case 'sqlsrv':return "$this->driver:Server=$address,$port;Database=$database";
  2840. }
  2841. }
  2842. private function getCommands(): array
  2843. {
  2844. switch ($this->driver) {
  2845. case 'mysql':return [
  2846. 'SET SESSION sql_warnings=1;',
  2847. 'SET NAMES utf8mb4;',
  2848. 'SET SESSION sql_mode = "ANSI,TRADITIONAL";',
  2849. ];
  2850. case 'pgsql':return [
  2851. "SET NAMES 'UTF8';",
  2852. ];
  2853. case 'sqlsrv':return [
  2854. ];
  2855. }
  2856. }
  2857. private function getOptions(): array
  2858. {
  2859. $options = array(
  2860. \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
  2861. \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
  2862. );
  2863. switch ($this->driver) {
  2864. case 'mysql':return $options + [
  2865. \PDO::ATTR_EMULATE_PREPARES => false,
  2866. \PDO::MYSQL_ATTR_FOUND_ROWS => true,
  2867. \PDO::ATTR_PERSISTENT => true,
  2868. ];
  2869. case 'pgsql':return $options + [
  2870. \PDO::ATTR_EMULATE_PREPARES => false,
  2871. \PDO::ATTR_PERSISTENT => true,
  2872. ];
  2873. case 'sqlsrv':return $options + [
  2874. \PDO::SQLSRV_ATTR_DIRECT_QUERY => false,
  2875. \PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
  2876. ];
  2877. }
  2878. }
  2879. public function __construct(string $driver, string $address, string $port = null, string $database = null, string $username = null, string $password = null)
  2880. {
  2881. $this->driver = $driver;
  2882. $this->database = $database;
  2883. $dsn = $this->getDsn($address, $port, $database);
  2884. $options = $this->getOptions();
  2885. $this->pdo = new \PDO($dsn, $username, $password, $options);
  2886. $commands = $this->getCommands();
  2887. foreach ($commands as $command) {
  2888. $this->pdo->query($command);
  2889. }
  2890. $this->reflection = new GenericReflection($this->pdo, $driver, $database);
  2891. $this->definition = new GenericDefinition($this->pdo, $driver, $database);
  2892. $this->conditions = new ConditionsBuilder($driver);
  2893. $this->columns = new ColumnsBuilder($driver);
  2894. $this->converter = new DataConverter($driver);
  2895. }
  2896. public function pdo(): \PDO
  2897. {
  2898. return $this->pdo;
  2899. }
  2900. public function reflection(): GenericReflection
  2901. {
  2902. return $this->reflection;
  2903. }
  2904. public function definition(): GenericDefinition
  2905. {
  2906. return $this->definition;
  2907. }
  2908. private function addMiddlewareConditions(string $tableName, Condition $condition): Condition
  2909. {
  2910. $condition1 = VariableStore::get("authorization.conditions.$tableName");
  2911. if ($condition1) {
  2912. $condition = $condition->_and($condition1);
  2913. }
  2914. $condition2 = VariableStore::get("multiTenancy.conditions.$tableName");
  2915. if ($condition2) {
  2916. $condition = $condition->_and($condition2);
  2917. }
  2918. return $condition;
  2919. }
  2920. public function createSingle(ReflectedTable $table, array $columnValues) /*: ?String*/
  2921. {
  2922. $this->converter->convertColumnValues($table, $columnValues);
  2923. $insertColumns = $this->columns->getInsert($table, $columnValues);
  2924. $tableName = $table->getName();
  2925. $pkName = $table->getPk()->getName();
  2926. $parameters = array_values($columnValues);
  2927. $sql = 'INSERT INTO "' . $tableName . '" ' . $insertColumns;
  2928. $stmt = $this->query($sql, $parameters);
  2929. if (isset($columnValues[$pkName])) {
  2930. return $columnValues[$pkName];
  2931. }
  2932. switch ($this->driver) {
  2933. case 'mysql':
  2934. $stmt = $this->query('SELECT LAST_INSERT_ID()', []);
  2935. break;
  2936. }
  2937. $pkValue = $stmt->fetchColumn(0);
  2938. if ($this->driver == 'sqlsrv' && $table->getPk()->getType() == 'bigint') {
  2939. return (int) $pkValue;
  2940. }
  2941. return $pkValue;
  2942. }
  2943. public function selectSingle(ReflectedTable $table, array $columnNames, string $id) /*: ?array*/
  2944. {
  2945. $selectColumns = $this->columns->getSelect($table, $columnNames);
  2946. $tableName = $table->getName();
  2947. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  2948. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2949. $parameters = array();
  2950. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  2951. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
  2952. $stmt = $this->query($sql, $parameters);
  2953. $record = $stmt->fetch() ?: null;
  2954. if ($record === null) {
  2955. return null;
  2956. }
  2957. $records = array($record);
  2958. $this->converter->convertRecords($table, $columnNames, $records);
  2959. return $records[0];
  2960. }
  2961. public function selectMultiple(ReflectedTable $table, array $columnNames, array $ids): array
  2962. {
  2963. if (count($ids) == 0) {
  2964. return [];
  2965. }
  2966. $selectColumns = $this->columns->getSelect($table, $columnNames);
  2967. $tableName = $table->getName();
  2968. $condition = new ColumnCondition($table->getPk(), 'in', implode(',', $ids));
  2969. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2970. $parameters = array();
  2971. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  2972. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
  2973. $stmt = $this->query($sql, $parameters);
  2974. $records = $stmt->fetchAll();
  2975. $this->converter->convertRecords($table, $columnNames, $records);
  2976. return $records;
  2977. }
  2978. public function selectCount(ReflectedTable $table, Condition $condition): int
  2979. {
  2980. $tableName = $table->getName();
  2981. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2982. $parameters = array();
  2983. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  2984. $sql = 'SELECT COUNT(*) FROM "' . $tableName . '"' . $whereClause;
  2985. $stmt = $this->query($sql, $parameters);
  2986. return $stmt->fetchColumn(0);
  2987. }
  2988. public function selectAll(ReflectedTable $table, array $columnNames, Condition $condition, array $columnOrdering, int $offset, int $limit): array
  2989. {
  2990. if ($limit == 0) {
  2991. return array();
  2992. }
  2993. $selectColumns = $this->columns->getSelect($table, $columnNames);
  2994. $tableName = $table->getName();
  2995. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2996. $parameters = array();
  2997. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  2998. $orderBy = $this->columns->getOrderBy($table, $columnOrdering);
  2999. $offsetLimit = $this->columns->getOffsetLimit($offset, $limit);
  3000. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '"' . $whereClause . $orderBy . $offsetLimit;
  3001. $stmt = $this->query($sql, $parameters);
  3002. $records = $stmt->fetchAll();
  3003. $this->converter->convertRecords($table, $columnNames, $records);
  3004. return $records;
  3005. }
  3006. public function updateSingle(ReflectedTable $table, array $columnValues, string $id)
  3007. {
  3008. if (count($columnValues) == 0) {
  3009. return 0;
  3010. }
  3011. $this->converter->convertColumnValues($table, $columnValues);
  3012. $updateColumns = $this->columns->getUpdate($table, $columnValues);
  3013. $tableName = $table->getName();
  3014. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  3015. $condition = $this->addMiddlewareConditions($tableName, $condition);
  3016. $parameters = array_values($columnValues);
  3017. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  3018. $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
  3019. $stmt = $this->query($sql, $parameters);
  3020. return $stmt->rowCount();
  3021. }
  3022. public function deleteSingle(ReflectedTable $table, string $id)
  3023. {
  3024. $tableName = $table->getName();
  3025. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  3026. $condition = $this->addMiddlewareConditions($tableName, $condition);
  3027. $parameters = array();
  3028. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  3029. $sql = 'DELETE FROM "' . $tableName . '" ' . $whereClause;
  3030. $stmt = $this->query($sql, $parameters);
  3031. return $stmt->rowCount();
  3032. }
  3033. public function incrementSingle(ReflectedTable $table, array $columnValues, string $id)
  3034. {
  3035. if (count($columnValues) == 0) {
  3036. return 0;
  3037. }
  3038. $this->converter->convertColumnValues($table, $columnValues);
  3039. $updateColumns = $this->columns->getIncrement($table, $columnValues);
  3040. $tableName = $table->getName();
  3041. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  3042. $condition = $this->addMiddlewareConditions($tableName, $condition);
  3043. $parameters = array_values($columnValues);
  3044. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  3045. $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
  3046. $stmt = $this->query($sql, $parameters);
  3047. return $stmt->rowCount();
  3048. }
  3049. private function query(string $sql, array $parameters): \PDOStatement
  3050. {
  3051. $stmt = $this->pdo->prepare($sql);
  3052. $stmt->execute($parameters);
  3053. return $stmt;
  3054. }
  3055. }
  3056. // file: src/Tqdev/PhpCrudApi/Database/GenericDefinition.php
  3057. class GenericDefinition
  3058. {
  3059. private $pdo;
  3060. private $driver;
  3061. private $database;
  3062. private $typeConverter;
  3063. private $reflection;
  3064. public function __construct(\PDO $pdo, string $driver, string $database)
  3065. {
  3066. $this->pdo = $pdo;
  3067. $this->driver = $driver;
  3068. $this->database = $database;
  3069. $this->typeConverter = new TypeConverter($driver);
  3070. $this->reflection = new GenericReflection($pdo, $driver, $database);
  3071. }
  3072. private function quote(string $identifier): string
  3073. {
  3074. return '"' . str_replace('"', '', $identifier) . '"';
  3075. }
  3076. public function getColumnType(ReflectedColumn $column, bool $update): string
  3077. {
  3078. if ($this->driver == 'pgsql' && !$update && $column->getPk() && $this->canAutoIncrement($column)) {
  3079. return 'serial';
  3080. }
  3081. $type = $this->typeConverter->fromJdbc($column->getType());
  3082. if ($column->hasPrecision() && $column->hasScale()) {
  3083. $size = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
  3084. } else if ($column->hasPrecision()) {
  3085. $size = '(' . $column->getPrecision() . ')';
  3086. } else if ($column->hasLength()) {
  3087. $size = '(' . $column->getLength() . ')';
  3088. } else {
  3089. $size = '';
  3090. }
  3091. $null = $this->getColumnNullType($column, $update);
  3092. $auto = $this->getColumnAutoIncrement($column, $update);
  3093. return $type . $size . $null . $auto;
  3094. }
  3095. private function getPrimaryKey(string $tableName): string
  3096. {
  3097. $pks = $this->reflection->getTablePrimaryKeys($tableName);
  3098. if (count($pks) == 1) {
  3099. return $pks[0];
  3100. }
  3101. return "";
  3102. }
  3103. private function canAutoIncrement(ReflectedColumn $column): bool
  3104. {
  3105. return in_array($column->getType(), ['integer', 'bigint']);
  3106. }
  3107. private function getColumnAutoIncrement(ReflectedColumn $column, bool $update): string
  3108. {
  3109. if (!$this->canAutoIncrement($column)) {
  3110. return '';
  3111. }
  3112. switch ($this->driver) {
  3113. case 'mysql':
  3114. return $column->getPk() ? ' AUTO_INCREMENT' : '';
  3115. case 'pgsql':
  3116. case 'sqlsrv':
  3117. return '';
  3118. }
  3119. }
  3120. private function getColumnNullType(ReflectedColumn $column, bool $update): string
  3121. {
  3122. if ($this->driver == 'pgsql' && $update) {
  3123. return '';
  3124. }
  3125. return $column->getNullable() ? ' NULL' : ' NOT NULL';
  3126. }
  3127. private function getTableRenameSQL(string $tableName, string $newTableName): string
  3128. {
  3129. $p1 = $this->quote($tableName);
  3130. $p2 = $this->quote($newTableName);
  3131. switch ($this->driver) {
  3132. case 'mysql':
  3133. return "RENAME TABLE $p1 TO $p2";
  3134. case 'pgsql':
  3135. return "ALTER TABLE $p1 RENAME TO $p2";
  3136. case 'sqlsrv':
  3137. return "EXEC sp_rename $p1, $p2";
  3138. }
  3139. }
  3140. private function getColumnRenameSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3141. {
  3142. $p1 = $this->quote($tableName);
  3143. $p2 = $this->quote($columnName);
  3144. $p3 = $this->quote($newColumn->getName());
  3145. switch ($this->driver) {
  3146. case 'mysql':
  3147. $p4 = $this->getColumnType($newColumn, true);
  3148. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3149. case 'pgsql':
  3150. return "ALTER TABLE $p1 RENAME COLUMN $p2 TO $p3";
  3151. case 'sqlsrv':
  3152. $p4 = $this->quote($tableName . '.' . $columnName);
  3153. return "EXEC sp_rename $p4, $p3, 'COLUMN'";
  3154. }
  3155. }
  3156. private function getColumnRetypeSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3157. {
  3158. $p1 = $this->quote($tableName);
  3159. $p2 = $this->quote($columnName);
  3160. $p3 = $this->quote($newColumn->getName());
  3161. $p4 = $this->getColumnType($newColumn, true);
  3162. switch ($this->driver) {
  3163. case 'mysql':
  3164. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3165. case 'pgsql':
  3166. return "ALTER TABLE $p1 ALTER COLUMN $p3 TYPE $p4";
  3167. case 'sqlsrv':
  3168. return "ALTER TABLE $p1 ALTER COLUMN $p3 $p4";
  3169. }
  3170. }
  3171. private function getSetColumnNullableSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3172. {
  3173. $p1 = $this->quote($tableName);
  3174. $p2 = $this->quote($columnName);
  3175. $p3 = $this->quote($newColumn->getName());
  3176. $p4 = $this->getColumnType($newColumn, true);
  3177. switch ($this->driver) {
  3178. case 'mysql':
  3179. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3180. case 'pgsql':
  3181. $p5 = $newColumn->getNullable() ? 'DROP NOT NULL' : 'SET NOT NULL';
  3182. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p5";
  3183. case 'sqlsrv':
  3184. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
  3185. }
  3186. }
  3187. private function getSetColumnPkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3188. {
  3189. $p1 = $this->quote($tableName);
  3190. $p2 = $this->quote($columnName);
  3191. $p3 = $this->quote($tableName . '_pkey');
  3192. switch ($this->driver) {
  3193. case 'mysql':
  3194. $p4 = $newColumn->getPk() ? "ADD PRIMARY KEY ($p2)" : 'DROP PRIMARY KEY';
  3195. return "ALTER TABLE $p1 $p4";
  3196. case 'pgsql':
  3197. case 'sqlsrv':
  3198. $p4 = $newColumn->getPk() ? "ADD CONSTRAINT $p3 PRIMARY KEY ($p2)" : "DROP CONSTRAINT $p3";
  3199. return "ALTER TABLE $p1 $p4";
  3200. }
  3201. }
  3202. private function getSetColumnPkSequenceSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3203. {
  3204. $p1 = $this->quote($tableName);
  3205. $p2 = $this->quote($columnName);
  3206. $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
  3207. switch ($this->driver) {
  3208. case 'mysql':
  3209. return "select 1";
  3210. case 'pgsql':
  3211. return $newColumn->getPk() ? "CREATE SEQUENCE $p3 OWNED BY $p1.$p2" : "DROP SEQUENCE $p3";
  3212. case 'sqlsrv':
  3213. return $newColumn->getPk() ? "CREATE SEQUENCE $p3" : "DROP SEQUENCE $p3";
  3214. }
  3215. }
  3216. private function getSetColumnPkSequenceStartSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3217. {
  3218. $p1 = $this->quote($tableName);
  3219. $p2 = $this->quote($columnName);
  3220. switch ($this->driver) {
  3221. case 'mysql':
  3222. return "select 1";
  3223. case 'pgsql':
  3224. $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
  3225. return "SELECT setval($p3, (SELECT max($p2)+1 FROM $p1));";
  3226. case 'sqlsrv':
  3227. $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
  3228. $p4 = $this->pdo->query("SELECT max($p2)+1 FROM $p1")->fetchColumn();
  3229. return "ALTER SEQUENCE $p3 RESTART WITH $p4";
  3230. }
  3231. }
  3232. private function getSetColumnPkDefaultSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3233. {
  3234. $p1 = $this->quote($tableName);
  3235. $p2 = $this->quote($columnName);
  3236. switch ($this->driver) {
  3237. case 'mysql':
  3238. $p3 = $this->quote($newColumn->getName());
  3239. $p4 = $this->getColumnType($newColumn, true);
  3240. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3241. case 'pgsql':
  3242. if ($newColumn->getPk()) {
  3243. $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
  3244. $p4 = "SET DEFAULT nextval($p3)";
  3245. } else {
  3246. $p4 = 'DROP DEFAULT';
  3247. }
  3248. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
  3249. case 'sqlsrv':
  3250. $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
  3251. $p4 = $this->quote($tableName . '_' . $columnName . '_def');
  3252. if ($newColumn->getPk()) {
  3253. return "ALTER TABLE $p1 ADD CONSTRAINT $p4 DEFAULT NEXT VALUE FOR $p3 FOR $p2";
  3254. } else {
  3255. return "ALTER TABLE $p1 DROP CONSTRAINT $p4";
  3256. }
  3257. }
  3258. }
  3259. private function getAddColumnFkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3260. {
  3261. $p1 = $this->quote($tableName);
  3262. $p2 = $this->quote($columnName);
  3263. $p3 = $this->quote($tableName . '_' . $columnName . '_fkey');
  3264. $p4 = $this->quote($newColumn->getFk());
  3265. $p5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
  3266. return "ALTER TABLE $p1 ADD CONSTRAINT $p3 FOREIGN KEY ($p2) REFERENCES $p4 ($p5)";
  3267. }
  3268. private function getRemoveColumnFkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3269. {
  3270. $p1 = $this->quote($tableName);
  3271. $p2 = $this->quote($tableName . '_' . $columnName . '_fkey');
  3272. switch ($this->driver) {
  3273. case 'mysql':
  3274. return "ALTER TABLE $p1 DROP FOREIGN KEY $p2";
  3275. case 'pgsql':
  3276. case 'sqlsrv':
  3277. return "ALTER TABLE $p1 DROP CONSTRAINT $p2";
  3278. }
  3279. }
  3280. private function getAddTableSQL(ReflectedTable $newTable): string
  3281. {
  3282. $tableName = $newTable->getName();
  3283. $p1 = $this->quote($tableName);
  3284. $fields = [];
  3285. $constraints = [];
  3286. foreach ($newTable->getColumnNames() as $columnName) {
  3287. $pkColumn = $this->getPrimaryKey($tableName);
  3288. $newColumn = $newTable->getColumn($columnName);
  3289. $f1 = $this->quote($columnName);
  3290. $f2 = $this->getColumnType($newColumn, false);
  3291. $f3 = $this->quote($tableName . '_' . $columnName . '_fkey');
  3292. $f4 = $this->quote($newColumn->getFk());
  3293. $f5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
  3294. $f6 = $this->quote($tableName . '_' . $pkColumn . '_pkey');
  3295. $fields[] = "$f1 $f2";
  3296. if ($newColumn->getPk()) {
  3297. $constraints[] = "CONSTRAINT $f6 PRIMARY KEY ($f1)";
  3298. }
  3299. if ($newColumn->getFk()) {
  3300. $constraints[] = "CONSTRAINT $f3 FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
  3301. }
  3302. }
  3303. $p2 = implode(',', array_merge($fields, $constraints));
  3304. return "CREATE TABLE $p1 ($p2);";
  3305. }
  3306. private function getAddColumnSQL(string $tableName, ReflectedColumn $newColumn): string
  3307. {
  3308. $p1 = $this->quote($tableName);
  3309. $p2 = $this->quote($newColumn->getName());
  3310. $p3 = $this->getColumnType($newColumn, false);
  3311. switch ($this->driver) {
  3312. case 'mysql':
  3313. case 'pgsql':
  3314. return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
  3315. case 'sqlsrv':
  3316. return "ALTER TABLE $p1 ADD $p2 $p3";
  3317. }
  3318. }
  3319. private function getRemoveTableSQL(string $tableName): string
  3320. {
  3321. $p1 = $this->quote($tableName);
  3322. switch ($this->driver) {
  3323. case 'mysql':
  3324. case 'pgsql':
  3325. return "DROP TABLE $p1 CASCADE;";
  3326. case 'sqlsrv':
  3327. return "DROP TABLE $p1;";
  3328. }
  3329. }
  3330. private function getRemoveColumnSQL(string $tableName, string $columnName): string
  3331. {
  3332. $p1 = $this->quote($tableName);
  3333. $p2 = $this->quote($columnName);
  3334. switch ($this->driver) {
  3335. case 'mysql':
  3336. case 'pgsql':
  3337. return "ALTER TABLE $p1 DROP COLUMN $p2 CASCADE;";
  3338. case 'sqlsrv':
  3339. return "ALTER TABLE $p1 DROP COLUMN $p2;";
  3340. }
  3341. }
  3342. public function renameTable(string $tableName, string $newTableName)
  3343. {
  3344. $sql = $this->getTableRenameSQL($tableName, $newTableName);
  3345. return $this->query($sql);
  3346. }
  3347. public function renameColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3348. {
  3349. $sql = $this->getColumnRenameSQL($tableName, $columnName, $newColumn);
  3350. return $this->query($sql);
  3351. }
  3352. public function retypeColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3353. {
  3354. $sql = $this->getColumnRetypeSQL($tableName, $columnName, $newColumn);
  3355. return $this->query($sql);
  3356. }
  3357. public function setColumnNullable(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3358. {
  3359. $sql = $this->getSetColumnNullableSQL($tableName, $columnName, $newColumn);
  3360. return $this->query($sql);
  3361. }
  3362. public function addColumnPrimaryKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3363. {
  3364. $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
  3365. $this->query($sql);
  3366. if ($this->canAutoIncrement($newColumn)) {
  3367. $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
  3368. $this->query($sql);
  3369. $sql = $this->getSetColumnPkSequenceStartSQL($tableName, $columnName, $newColumn);
  3370. $this->query($sql);
  3371. $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
  3372. $this->query($sql);
  3373. }
  3374. return true;
  3375. }
  3376. public function removeColumnPrimaryKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3377. {
  3378. if ($this->canAutoIncrement($newColumn)) {
  3379. $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
  3380. $this->query($sql);
  3381. $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
  3382. $this->query($sql);
  3383. }
  3384. $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
  3385. $this->query($sql);
  3386. return true;
  3387. }
  3388. public function addColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3389. {
  3390. $sql = $this->getAddColumnFkConstraintSQL($tableName, $columnName, $newColumn);
  3391. return $this->query($sql);
  3392. }
  3393. public function removeColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3394. {
  3395. $sql = $this->getRemoveColumnFkConstraintSQL($tableName, $columnName, $newColumn);
  3396. return $this->query($sql);
  3397. }
  3398. public function addTable(ReflectedTable $newTable)
  3399. {
  3400. $sql = $this->getAddTableSQL($newTable);
  3401. return $this->query($sql);
  3402. }
  3403. public function addColumn(string $tableName, ReflectedColumn $newColumn)
  3404. {
  3405. $sql = $this->getAddColumnSQL($tableName, $newColumn);
  3406. return $this->query($sql);
  3407. }
  3408. public function removeTable(string $tableName)
  3409. {
  3410. $sql = $this->getRemoveTableSQL($tableName);
  3411. return $this->query($sql);
  3412. }
  3413. public function removeColumn(string $tableName, string $columnName)
  3414. {
  3415. $sql = $this->getRemoveColumnSQL($tableName, $columnName);
  3416. return $this->query($sql);
  3417. }
  3418. private function query(string $sql): bool
  3419. {
  3420. $stmt = $this->pdo->prepare($sql);
  3421. return $stmt->execute();
  3422. }
  3423. }
  3424. // file: src/Tqdev/PhpCrudApi/Database/GenericReflection.php
  3425. class GenericReflection
  3426. {
  3427. private $pdo;
  3428. private $driver;
  3429. private $database;
  3430. private $typeConverter;
  3431. public function __construct(\PDO $pdo, string $driver, string $database)
  3432. {
  3433. $this->pdo = $pdo;
  3434. $this->driver = $driver;
  3435. $this->database = $database;
  3436. $this->typeConverter = new TypeConverter($driver);
  3437. }
  3438. public function getIgnoredTables(): array
  3439. {
  3440. switch ($this->driver) {
  3441. case 'mysql':return [];
  3442. case 'pgsql':return ['spatial_ref_sys', 'raster_columns', 'raster_overviews', 'geography_columns', 'geometry_columns'];
  3443. case 'sqlsrv':return [];
  3444. }
  3445. }
  3446. private function getTablesSQL(): string
  3447. {
  3448. switch ($this->driver) {
  3449. 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"';
  3450. 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";';
  3451. 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"';
  3452. }
  3453. }
  3454. private function getTableColumnsSQL(): string
  3455. {
  3456. switch ($this->driver) {
  3457. 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" = ?';
  3458. 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;';
  3459. 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 \'\' <> ?';
  3460. }
  3461. }
  3462. private function getTablePrimaryKeysSQL(): string
  3463. {
  3464. switch ($this->driver) {
  3465. case 'mysql':return 'SELECT "COLUMN_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "CONSTRAINT_NAME" = \'PRIMARY\' AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
  3466. 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\'';
  3467. 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 \'\' <> ?';
  3468. }
  3469. }
  3470. private function getTableForeignKeysSQL(): string
  3471. {
  3472. switch ($this->driver) {
  3473. 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" = ?';
  3474. 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\'';
  3475. 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 \'\' <> ?';
  3476. }
  3477. }
  3478. public function getDatabaseName(): string
  3479. {
  3480. return $this->database;
  3481. }
  3482. public function getTables(): array
  3483. {
  3484. $sql = $this->getTablesSQL();
  3485. $results = $this->query($sql, [$this->database]);
  3486. foreach ($results as &$result) {
  3487. switch ($this->driver) {
  3488. case 'mysql':
  3489. $map = ['BASE TABLE' => 'table', 'VIEW' => 'view'];
  3490. $result['TABLE_TYPE'] = $map[$result['TABLE_TYPE']];
  3491. break;
  3492. case 'pgsql':
  3493. $map = ['r' => 'table', 'v' => 'view'];
  3494. $result['TABLE_TYPE'] = $map[$result['TABLE_TYPE']];
  3495. break;
  3496. case 'sqlsrv':
  3497. $map = ['U' => 'table', 'V' => 'view'];
  3498. $result['TABLE_TYPE'] = $map[trim($result['TABLE_TYPE'])];
  3499. break;
  3500. }
  3501. }
  3502. return $results;
  3503. }
  3504. public function getTableColumns(string $tableName, string $type): array
  3505. {
  3506. $sql = $this->getTableColumnsSQL();
  3507. $results = $this->query($sql, [$tableName, $this->database]);
  3508. if ($type == 'view') {
  3509. foreach ($results as &$result) {
  3510. $result['IS_NULLABLE'] = false;
  3511. }
  3512. }
  3513. return $results;
  3514. }
  3515. public function getTablePrimaryKeys(string $tableName): array
  3516. {
  3517. $sql = $this->getTablePrimaryKeysSQL();
  3518. $results = $this->query($sql, [$tableName, $this->database]);
  3519. $primaryKeys = [];
  3520. foreach ($results as $result) {
  3521. $primaryKeys[] = $result['COLUMN_NAME'];
  3522. }
  3523. return $primaryKeys;
  3524. }
  3525. public function getTableForeignKeys(string $tableName): array
  3526. {
  3527. $sql = $this->getTableForeignKeysSQL();
  3528. $results = $this->query($sql, [$tableName, $this->database]);
  3529. $foreignKeys = [];
  3530. foreach ($results as $result) {
  3531. $foreignKeys[$result['COLUMN_NAME']] = $result['REFERENCED_TABLE_NAME'];
  3532. }
  3533. return $foreignKeys;
  3534. }
  3535. public function toJdbcType(string $type, int $size): string
  3536. {
  3537. return $this->typeConverter->toJdbc($type, $size);
  3538. }
  3539. private function query(string $sql, array $parameters): array
  3540. {
  3541. $stmt = $this->pdo->prepare($sql);
  3542. $stmt->execute($parameters);
  3543. return $stmt->fetchAll();
  3544. }
  3545. }
  3546. // file: src/Tqdev/PhpCrudApi/Database/TypeConverter.php
  3547. class TypeConverter
  3548. {
  3549. private $driver;
  3550. public function __construct(string $driver)
  3551. {
  3552. $this->driver = $driver;
  3553. }
  3554. private $fromJdbc = [
  3555. 'mysql' => [
  3556. 'clob' => 'longtext',
  3557. 'boolean' => 'bit',
  3558. 'blob' => 'longblob',
  3559. 'timestamp' => 'datetime',
  3560. ],
  3561. 'pgsql' => [
  3562. 'clob' => 'text',
  3563. 'blob' => 'bytea',
  3564. ],
  3565. 'sqlsrv' => [
  3566. 'boolean' => 'bit',
  3567. 'varchar' => 'nvarchar',
  3568. 'clob' => 'ntext',
  3569. 'blob' => 'image',
  3570. ],
  3571. ];
  3572. private $toJdbc = [
  3573. 'simplified' => [
  3574. 'char' => 'varchar',
  3575. 'longvarchar' => 'clob',
  3576. 'nchar' => 'varchar',
  3577. 'nvarchar' => 'varchar',
  3578. 'longnvarchar' => 'clob',
  3579. 'binary' => 'varbinary',
  3580. 'longvarbinary' => 'blob',
  3581. 'tinyint' => 'integer',
  3582. 'smallint' => 'integer',
  3583. 'real' => 'float',
  3584. 'numeric' => 'decimal',
  3585. 'time_with_timezone' => 'time',
  3586. 'timestamp_with_timezone' => 'timestamp',
  3587. ],
  3588. 'mysql' => [
  3589. 'bit' => 'boolean',
  3590. 'tinyblob' => 'blob',
  3591. 'mediumblob' => 'blob',
  3592. 'longblob' => 'blob',
  3593. 'tinytext' => 'clob',
  3594. 'mediumtext' => 'clob',
  3595. 'longtext' => 'clob',
  3596. 'text' => 'clob',
  3597. 'mediumint' => 'integer',
  3598. 'int' => 'integer',
  3599. 'polygon' => 'geometry',
  3600. 'point' => 'geometry',
  3601. 'datetime' => 'timestamp',
  3602. 'year' => 'integer',
  3603. 'enum' => 'varchar',
  3604. 'json' => 'clob',
  3605. ],
  3606. 'pgsql' => [
  3607. 'bigserial' => 'bigint',
  3608. 'bit varying' => 'bit',
  3609. 'box' => 'geometry',
  3610. 'bytea' => 'blob',
  3611. 'character varying' => 'varchar',
  3612. 'character' => 'char',
  3613. 'cidr' => 'varchar',
  3614. 'circle' => 'geometry',
  3615. 'double precision' => 'double',
  3616. 'inet' => 'integer',
  3617. 'json' => 'clob',
  3618. 'jsonb' => 'clob',
  3619. 'line' => 'geometry',
  3620. 'lseg' => 'geometry',
  3621. 'macaddr' => 'varchar',
  3622. 'money' => 'decimal',
  3623. 'path' => 'geometry',
  3624. 'point' => 'geometry',
  3625. 'polygon' => 'geometry',
  3626. 'real' => 'float',
  3627. 'serial' => 'integer',
  3628. 'text' => 'clob',
  3629. 'time without time zone' => 'time',
  3630. 'time with time zone' => 'time_with_timezone',
  3631. 'timestamp without time zone' => 'timestamp',
  3632. 'timestamp with time zone' => 'timestamp_with_timezone',
  3633. 'uuid' => 'char',
  3634. 'xml' => 'clob',
  3635. ],
  3636. 'sqlsrv' => [
  3637. 'varbinary(0)' => 'blob',
  3638. 'bit' => 'boolean',
  3639. 'datetime' => 'timestamp',
  3640. 'datetime2' => 'timestamp',
  3641. 'float' => 'double',
  3642. 'image' => 'blob',
  3643. 'int' => 'integer',
  3644. 'money' => 'decimal',
  3645. 'ntext' => 'clob',
  3646. 'smalldatetime' => 'timestamp',
  3647. 'smallmoney' => 'decimal',
  3648. 'text' => 'clob',
  3649. 'timestamp' => 'binary',
  3650. 'udt' => 'varbinary',
  3651. 'uniqueidentifier' => 'char',
  3652. 'xml' => 'clob',
  3653. ],
  3654. ];
  3655. private $valid = [
  3656. 'bigint' => true,
  3657. 'binary' => true,
  3658. 'bit' => true,
  3659. 'blob' => true,
  3660. 'boolean' => true,
  3661. 'char' => true,
  3662. 'clob' => true,
  3663. 'date' => true,
  3664. 'decimal' => true,
  3665. 'distinct' => true,
  3666. 'double' => true,
  3667. 'float' => true,
  3668. 'integer' => true,
  3669. 'longnvarchar' => true,
  3670. 'longvarbinary' => true,
  3671. 'longvarchar' => true,
  3672. 'nchar' => true,
  3673. 'nclob' => true,
  3674. 'numeric' => true,
  3675. 'nvarchar' => true,
  3676. 'real' => true,
  3677. 'smallint' => true,
  3678. 'time' => true,
  3679. 'time_with_timezone' => true,
  3680. 'timestamp' => true,
  3681. 'timestamp_with_timezone' => true,
  3682. 'tinyint' => true,
  3683. 'varbinary' => true,
  3684. 'varchar' => true,
  3685. 'geometry' => true,
  3686. ];
  3687. public function toJdbc(string $type, int $size): string
  3688. {
  3689. $jdbcType = strtolower($type);
  3690. if (isset($this->toJdbc[$this->driver]["$jdbcType($size)"])) {
  3691. $jdbcType = $this->toJdbc[$this->driver]["$jdbcType($size)"];
  3692. }
  3693. if (isset($this->toJdbc[$this->driver][$jdbcType])) {
  3694. $jdbcType = $this->toJdbc[$this->driver][$jdbcType];
  3695. }
  3696. if (isset($this->toJdbc['simplified'][$jdbcType])) {
  3697. $jdbcType = $this->toJdbc['simplified'][$jdbcType];
  3698. }
  3699. if (!isset($this->valid[$jdbcType])) {
  3700. throw new \Exception("Unsupported type '$jdbcType' for driver '$this->driver'");
  3701. }
  3702. return $jdbcType;
  3703. }
  3704. public function fromJdbc(string $type): string
  3705. {
  3706. $jdbcType = strtolower($type);
  3707. if (isset($this->fromJdbc[$this->driver][$jdbcType])) {
  3708. $jdbcType = $this->fromJdbc[$this->driver][$jdbcType];
  3709. }
  3710. return $jdbcType;
  3711. }
  3712. }
  3713. // file: src/Tqdev/PhpCrudApi/Middleware/Base/Middleware.php
  3714. abstract class Middleware implements MiddlewareInterface
  3715. {
  3716. protected $next;
  3717. protected $responder;
  3718. private $properties;
  3719. public function __construct(Router $router, Responder $responder, array $properties)
  3720. {
  3721. $router->load($this);
  3722. $this->responder = $responder;
  3723. $this->properties = $properties;
  3724. }
  3725. protected function getArrayProperty(string $key, string $default): array
  3726. {
  3727. return array_filter(array_map('trim', explode(',', $this->getProperty($key, $default))));
  3728. }
  3729. protected function getProperty(string $key, $default)
  3730. {
  3731. return isset($this->properties[$key]) ? $this->properties[$key] : $default;
  3732. }
  3733. }
  3734. // file: src/Tqdev/PhpCrudApi/Middleware/Communication/VariableStore.php
  3735. class VariableStore
  3736. {
  3737. static $values = array();
  3738. public static function get(string $key)
  3739. {
  3740. if (isset(self::$values[$key])) {
  3741. return self::$values[$key];
  3742. }
  3743. return null;
  3744. }
  3745. public static function set(string $key, /* object */ $value)
  3746. {
  3747. self::$values[$key] = $value;
  3748. }
  3749. }
  3750. // file: src/Tqdev/PhpCrudApi/Middleware/Router/Router.php
  3751. interface Router extends RequestHandlerInterface
  3752. {
  3753. public function register(string $method, string $path, array $handler);
  3754. public function load(Middleware $middleware);
  3755. public function route(ServerRequestInterface $request): ResponseInterface;
  3756. }
  3757. // file: src/Tqdev/PhpCrudApi/Middleware/Router/SimpleRouter.php
  3758. class SimpleRouter implements Router
  3759. {
  3760. private $responder;
  3761. private $cache;
  3762. private $ttl;
  3763. private $debug;
  3764. private $registration;
  3765. private $routes;
  3766. private $routeHandlers;
  3767. private $middlewares;
  3768. public function __construct(Responder $responder, Cache $cache, int $ttl, bool $debug)
  3769. {
  3770. $this->responder = $responder;
  3771. $this->cache = $cache;
  3772. $this->ttl = $ttl;
  3773. $this->debug = $debug;
  3774. $this->registration = true;
  3775. $this->routes = $this->loadPathTree();
  3776. $this->routeHandlers = [];
  3777. $this->middlewares = array();
  3778. }
  3779. private function loadPathTree(): PathTree
  3780. {
  3781. $data = $this->cache->get('PathTree');
  3782. if ($data != '') {
  3783. $tree = PathTree::fromJson(json_decode(gzuncompress($data)));
  3784. $this->registration = false;
  3785. } else {
  3786. $tree = new PathTree();
  3787. }
  3788. return $tree;
  3789. }
  3790. public function register(string $method, string $path, array $handler)
  3791. {
  3792. $routeNumber = count($this->routeHandlers);
  3793. $this->routeHandlers[$routeNumber] = $handler;
  3794. if ($this->registration) {
  3795. $parts = explode('/', trim($path, '/'));
  3796. array_unshift($parts, $method);
  3797. $this->routes->put($parts, $routeNumber);
  3798. }
  3799. }
  3800. public function load(Middleware $middleware) /*: void*/
  3801. {
  3802. array_push($this->middlewares, $middleware);
  3803. }
  3804. public function route(ServerRequestInterface $request): ResponseInterface
  3805. {
  3806. if ($this->registration) {
  3807. $data = gzcompress(json_encode($this->routes, JSON_UNESCAPED_UNICODE));
  3808. $this->cache->set('PathTree', $data, $this->ttl);
  3809. }
  3810. return $this->handle($request);
  3811. }
  3812. private function getRouteNumbers(ServerRequestInterface $request): array
  3813. {
  3814. $method = strtoupper($request->getMethod());
  3815. $path = explode('/', trim($request->getRequestTarget(), '/'));
  3816. array_unshift($path, $method);
  3817. return $this->routes->match($path);
  3818. }
  3819. public function handle(ServerRequestInterface $request): ResponseInterface
  3820. {
  3821. if (count($this->middlewares)) {
  3822. $handler = array_pop($this->middlewares);
  3823. return $handler->process($request, $this);
  3824. }
  3825. $routeNumbers = $this->getRouteNumbers($request);
  3826. if (count($routeNumbers) == 0) {
  3827. return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getRequestTarget());
  3828. }
  3829. try {
  3830. $response = call_user_func($this->routeHandlers[$routeNumbers[0]], $request);
  3831. } catch (\PDOException $e) {
  3832. if (strpos(strtolower($e->getMessage()), 'duplicate') !== false) {
  3833. $response = $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
  3834. } elseif (strpos(strtolower($e->getMessage()), 'default value') !== false) {
  3835. $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  3836. } elseif (strpos(strtolower($e->getMessage()), 'allow nulls') !== false) {
  3837. $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  3838. } elseif (strpos(strtolower($e->getMessage()), 'constraint') !== false) {
  3839. $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  3840. }
  3841. if ($this->debug) {
  3842. $response = ResponseUtils::addExceptionHeaders($response, $e);
  3843. }
  3844. }
  3845. return $response;
  3846. }
  3847. }
  3848. // file: src/Tqdev/PhpCrudApi/Middleware/AjaxOnlyMiddleware.php
  3849. class AjaxOnlyMiddleware extends Middleware
  3850. {
  3851. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  3852. {
  3853. $method = $request->getMethod();
  3854. $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
  3855. if (!in_array($method, $excludeMethods)) {
  3856. $headerName = $this->getProperty('headerName', 'X-Requested-With');
  3857. $headerValue = $this->getProperty('headerValue', 'XMLHttpRequest');
  3858. if ($headerValue != RequestUtils::getHeader($request, $headerName)) {
  3859. return $this->responder->error(ErrorCode::ONLY_AJAX_REQUESTS_ALLOWED, $method);
  3860. }
  3861. }
  3862. return $next->handle($request);
  3863. }
  3864. }
  3865. // file: src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php
  3866. class AuthorizationMiddleware extends Middleware
  3867. {
  3868. private $reflection;
  3869. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  3870. {
  3871. parent::__construct($router, $responder, $properties);
  3872. $this->reflection = $reflection;
  3873. }
  3874. private function handleColumns(string $operation, string $tableName) /*: void*/
  3875. {
  3876. $columnHandler = $this->getProperty('columnHandler', '');
  3877. if ($columnHandler) {
  3878. $table = $this->reflection->getTable($tableName);
  3879. foreach ($table->getColumnNames() as $columnName) {
  3880. $allowed = call_user_func($columnHandler, $operation, $tableName, $columnName);
  3881. if (!$allowed) {
  3882. $table->removeColumn($columnName);
  3883. }
  3884. }
  3885. }
  3886. }
  3887. private function handleTable(string $operation, string $tableName) /*: void*/
  3888. {
  3889. if (!$this->reflection->hasTable($tableName)) {
  3890. return;
  3891. }
  3892. $tableHandler = $this->getProperty('tableHandler', '');
  3893. if ($tableHandler) {
  3894. $allowed = call_user_func($tableHandler, $operation, $tableName);
  3895. if (!$allowed) {
  3896. $this->reflection->removeTable($tableName);
  3897. } else {
  3898. $this->handleColumns($operation, $tableName);
  3899. }
  3900. }
  3901. }
  3902. private function handleRecords(string $operation, string $tableName) /*: void*/
  3903. {
  3904. if (!$this->reflection->hasTable($tableName)) {
  3905. return;
  3906. }
  3907. $recordHandler = $this->getProperty('recordHandler', '');
  3908. if ($recordHandler) {
  3909. $query = call_user_func($recordHandler, $operation, $tableName);
  3910. $filters = new FilterInfo();
  3911. $table = $this->reflection->getTable($tableName);
  3912. $query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
  3913. parse_str($query, $params);
  3914. $condition = $filters->getCombinedConditions($table, $params);
  3915. VariableStore::set("authorization.conditions.$tableName", $condition);
  3916. }
  3917. }
  3918. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  3919. {
  3920. $path = RequestUtils::getPathSegment($request, 1);
  3921. $operation = RequestUtils::getOperation($request);
  3922. $tableNames = RequestUtils::getTableNames($request, $this->reflection);
  3923. foreach ($tableNames as $tableName) {
  3924. $this->handleTable($operation, $tableName);
  3925. if ($path == 'records') {
  3926. $this->handleRecords($operation, $tableName);
  3927. }
  3928. }
  3929. if ($path == 'openapi') {
  3930. VariableStore::set('authorization.tableHandler', $this->getProperty('tableHandler', ''));
  3931. VariableStore::set('authorization.columnHandler', $this->getProperty('columnHandler', ''));
  3932. }
  3933. return $next->handle($request);
  3934. }
  3935. }
  3936. // file: src/Tqdev/PhpCrudApi/Middleware/BasicAuthMiddleware.php
  3937. class BasicAuthMiddleware extends Middleware
  3938. {
  3939. private function hasCorrectPassword(string $username, string $password, array &$passwords): bool
  3940. {
  3941. $hash = isset($passwords[$username]) ? $passwords[$username] : false;
  3942. if ($hash && password_verify($password, $hash)) {
  3943. if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
  3944. $passwords[$username] = password_hash($password, PASSWORD_DEFAULT);
  3945. }
  3946. return true;
  3947. }
  3948. return false;
  3949. }
  3950. private function getValidUsername(string $username, string $password, string $passwordFile): string
  3951. {
  3952. $passwords = $this->readPasswords($passwordFile);
  3953. $valid = $this->hasCorrectPassword($username, $password, $passwords);
  3954. $this->writePasswords($passwordFile, $passwords);
  3955. return $valid ? $username : '';
  3956. }
  3957. private function readPasswords(string $passwordFile): array
  3958. {
  3959. $passwords = [];
  3960. $passwordLines = file($passwordFile);
  3961. foreach ($passwordLines as $passwordLine) {
  3962. if (strpos($passwordLine, ':') !== false) {
  3963. list($username, $hash) = explode(':', trim($passwordLine), 2);
  3964. if (strlen($hash) > 0 && $hash[0] != '$') {
  3965. $hash = password_hash($hash, PASSWORD_DEFAULT);
  3966. }
  3967. $passwords[$username] = $hash;
  3968. }
  3969. }
  3970. return $passwords;
  3971. }
  3972. private function writePasswords(string $passwordFile, array $passwords): bool
  3973. {
  3974. $success = false;
  3975. $passwordFileContents = '';
  3976. foreach ($passwords as $username => $hash) {
  3977. $passwordFileContents .= "$username:$hash\n";
  3978. }
  3979. if (file_get_contents($passwordFile) != $passwordFileContents) {
  3980. $success = file_put_contents($passwordFile, $passwordFileContents) !== false;
  3981. }
  3982. return $success;
  3983. }
  3984. private function getAuthorizationCredentials(ServerRequestInterface $request): string
  3985. {
  3986. if (isset($_SERVER['PHP_AUTH_USER'])) {
  3987. return $_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW'];
  3988. }
  3989. $header = RequestUtils::getHeader($request, 'Authorization');
  3990. $parts = explode(' ', trim($header), 2);
  3991. if (count($parts) != 2) {
  3992. return '';
  3993. }
  3994. if ($parts[0] != 'Basic') {
  3995. return '';
  3996. }
  3997. return base64_decode(strtr($parts[1], '-_', '+/'));
  3998. }
  3999. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4000. {
  4001. if (session_status() == PHP_SESSION_NONE) {
  4002. session_start();
  4003. }
  4004. $credentials = $this->getAuthorizationCredentials($request);
  4005. if ($credentials) {
  4006. list($username, $password) = array('', '');
  4007. if (strpos($credentials, ':') !== false) {
  4008. list($username, $password) = explode(':', $credentials, 2);
  4009. }
  4010. $passwordFile = $this->getProperty('passwordFile', '.htpasswd');
  4011. $validUser = $this->getValidUsername($username, $password, $passwordFile);
  4012. $_SESSION['username'] = $validUser;
  4013. if (!$validUser) {
  4014. return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
  4015. }
  4016. if (!headers_sent()) {
  4017. session_regenerate_id();
  4018. }
  4019. }
  4020. if (!isset($_SESSION['username']) || !$_SESSION['username']) {
  4021. $authenticationMode = $this->getProperty('mode', 'required');
  4022. if ($authenticationMode == 'required') {
  4023. $response = $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
  4024. $realm = $this->getProperty('realm', 'Username and password required');
  4025. $response = $response->withHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
  4026. return $response;
  4027. }
  4028. }
  4029. return $next->handle($request);
  4030. }
  4031. }
  4032. // file: src/Tqdev/PhpCrudApi/Middleware/CorsMiddleware.php
  4033. class CorsMiddleware extends Middleware
  4034. {
  4035. private function isOriginAllowed(string $origin, string $allowedOrigins): bool
  4036. {
  4037. $found = false;
  4038. foreach (explode(',', $allowedOrigins) as $allowedOrigin) {
  4039. $hostname = preg_quote(strtolower(trim($allowedOrigin)));
  4040. $regex = '/^' . str_replace('\*', '.*', $hostname) . '$/';
  4041. if (preg_match($regex, $origin)) {
  4042. $found = true;
  4043. break;
  4044. }
  4045. }
  4046. return $found;
  4047. }
  4048. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4049. {
  4050. $method = $request->getMethod();
  4051. $origin = count($request->getHeader('Origin')) ? $request->getHeader('Origin')[0] : '';
  4052. $allowedOrigins = $this->getProperty('allowedOrigins', '*');
  4053. if ($origin && !$this->isOriginAllowed($origin, $allowedOrigins)) {
  4054. $response = $this->responder->error(ErrorCode::ORIGIN_FORBIDDEN, $origin);
  4055. } elseif ($method == 'OPTIONS') {
  4056. $response = ResponseFactory::fromStatus(ResponseFactory::OK);
  4057. $allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization');
  4058. if ($allowHeaders) {
  4059. $response = $response->withHeader('Access-Control-Allow-Headers', $allowHeaders);
  4060. }
  4061. $allowMethods = $this->getProperty('allowMethods', 'OPTIONS, GET, PUT, POST, DELETE, PATCH');
  4062. if ($allowMethods) {
  4063. $response = $response->withHeader('Access-Control-Allow-Methods', $allowMethods);
  4064. }
  4065. $allowCredentials = $this->getProperty('allowCredentials', 'true');
  4066. if ($allowCredentials) {
  4067. $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials);
  4068. }
  4069. $maxAge = $this->getProperty('maxAge', '1728000');
  4070. if ($maxAge) {
  4071. $response = $response->withHeader('Access-Control-Max-Age', $maxAge);
  4072. }
  4073. $exposeHeaders = $this->getProperty('exposeHeaders', '');
  4074. if ($exposeHeaders) {
  4075. $response = $response->withHeader('Access-Control-Expose-Headers', $exposeHeaders);
  4076. }
  4077. } else {
  4078. $response = $next->handle($request);
  4079. }
  4080. if ($origin) {
  4081. $allowCredentials = $this->getProperty('allowCredentials', 'true');
  4082. if ($allowCredentials) {
  4083. $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials);
  4084. }
  4085. $response = $response->withHeader('Access-Control-Allow-Origin', $origin);
  4086. }
  4087. return $response;
  4088. }
  4089. }
  4090. // file: src/Tqdev/PhpCrudApi/Middleware/CustomizationMiddleware.php
  4091. class CustomizationMiddleware extends Middleware
  4092. {
  4093. private $reflection;
  4094. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4095. {
  4096. parent::__construct($router, $responder, $properties);
  4097. $this->reflection = $reflection;
  4098. }
  4099. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4100. {
  4101. $operation = RequestUtils::getOperation($request);
  4102. $tableName = RequestUtils::getPathSegment($request, 2);
  4103. $beforeHandler = $this->getProperty('beforeHandler', '');
  4104. $environment = (object) array();
  4105. if ($beforeHandler !== '') {
  4106. $result = call_user_func($beforeHandler, $operation, $tableName, $request, $environment);
  4107. $request = $result ?: $request;
  4108. }
  4109. $response = $next->handle($request);
  4110. $afterHandler = $this->getProperty('afterHandler', '');
  4111. if ($afterHandler !== '') {
  4112. $result = call_user_func($afterHandler, $operation, $tableName, $response, $environment);
  4113. $response = $result ?: $response;
  4114. }
  4115. return $response;
  4116. }
  4117. }
  4118. // file: src/Tqdev/PhpCrudApi/Middleware/FirewallMiddleware.php
  4119. class FirewallMiddleware extends Middleware
  4120. {
  4121. private function ipMatch(string $ip, string $cidr): bool
  4122. {
  4123. if (strpos($cidr, '/') !== false) {
  4124. list($subnet, $mask) = explode('/', trim($cidr));
  4125. if ((ip2long($ip) & ~((1 << (32 - $mask)) - 1)) == ip2long($subnet)) {
  4126. return true;
  4127. }
  4128. } else {
  4129. if (ip2long($ip) == ip2long($cidr)) {
  4130. return true;
  4131. }
  4132. }
  4133. return false;
  4134. }
  4135. private function isIpAllowed(string $ipAddress, string $allowedIpAddresses): bool
  4136. {
  4137. foreach (explode(',', $allowedIpAddresses) as $allowedIp) {
  4138. if ($this->ipMatch($ipAddress, $allowedIp)) {
  4139. return true;
  4140. }
  4141. }
  4142. return false;
  4143. }
  4144. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4145. {
  4146. $reverseProxy = $this->getProperty('reverseProxy', '');
  4147. if ($reverseProxy) {
  4148. $ipAddress = array_pop(explode(',', $request->getHeader('X-Forwarded-For')));
  4149. } elseif (isset($_SERVER['REMOTE_ADDR'])) {
  4150. $ipAddress = $_SERVER['REMOTE_ADDR'];
  4151. } else {
  4152. $ipAddress = '127.0.0.1';
  4153. }
  4154. $allowedIpAddresses = $this->getProperty('allowedIpAddresses', '');
  4155. if (!$this->isIpAllowed($ipAddress, $allowedIpAddresses)) {
  4156. $response = $this->responder->error(ErrorCode::TEMPORARY_OR_PERMANENTLY_BLOCKED, '');
  4157. } else {
  4158. $response = $next->handle($request);
  4159. }
  4160. return $response;
  4161. }
  4162. }
  4163. // file: src/Tqdev/PhpCrudApi/Middleware/IpAddressMiddleware.php
  4164. class IpAddressMiddleware extends Middleware
  4165. {
  4166. private $reflection;
  4167. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4168. {
  4169. parent::__construct($router, $responder, $properties);
  4170. $this->reflection = $reflection;
  4171. }
  4172. private function callHandler($record, string $operation, ReflectedTable $table) /*: object */
  4173. {
  4174. $context = (array) $record;
  4175. $columnNames = $this->getProperty('columns', '');
  4176. if ($columnNames) {
  4177. foreach (explode(',', $columnNames) as $columnName) {
  4178. if ($table->hasColumn($columnName)) {
  4179. if ($operation == 'create') {
  4180. $context[$columnName] = $_SERVER['REMOTE_ADDR'];
  4181. } else {
  4182. unset($context[$columnName]);
  4183. }
  4184. }
  4185. }
  4186. }
  4187. return (object) $context;
  4188. }
  4189. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4190. {
  4191. $operation = RequestUtils::getOperation($request);
  4192. if (in_array($operation, ['create', 'update', 'increment'])) {
  4193. $tableNames = $this->getProperty('tables', '');
  4194. $tableName = RequestUtils::getPathSegment($request, 2);
  4195. if (!$tableNames || in_array($tableName, explode(',', $tableNames))) {
  4196. if ($this->reflection->hasTable($tableName)) {
  4197. $record = $request->getParsedBody();
  4198. if ($record !== null) {
  4199. $table = $this->reflection->getTable($tableName);
  4200. if (is_array($record)) {
  4201. foreach ($record as &$r) {
  4202. $r = $this->callHandler($r, $operation, $table);
  4203. }
  4204. } else {
  4205. $record = $this->callHandler($record, $operation, $table);
  4206. }
  4207. $request = $request->withParsedBody($record);
  4208. }
  4209. }
  4210. }
  4211. }
  4212. return $next->handle($request);
  4213. }
  4214. }
  4215. // file: src/Tqdev/PhpCrudApi/Middleware/JoinLimitsMiddleware.php
  4216. class JoinLimitsMiddleware extends Middleware
  4217. {
  4218. private $reflection;
  4219. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4220. {
  4221. parent::__construct($router, $responder, $properties);
  4222. $this->reflection = $reflection;
  4223. }
  4224. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4225. {
  4226. $operation = RequestUtils::getOperation($request);
  4227. $params = RequestUtils::getParams($request);
  4228. if (in_array($operation, ['read', 'list']) && isset($params['join'])) {
  4229. $maxDepth = (int) $this->getProperty('depth', '3');
  4230. $maxTables = (int) $this->getProperty('tables', '10');
  4231. $maxRecords = (int) $this->getProperty('records', '1000');
  4232. $tableCount = 0;
  4233. $joinPaths = array();
  4234. for ($i = 0; $i < count($params['join']); $i++) {
  4235. $joinPath = array();
  4236. $tables = explode(',', $params['join'][$i]);
  4237. for ($depth = 0; $depth < min($maxDepth, count($tables)); $depth++) {
  4238. array_push($joinPath, $tables[$depth]);
  4239. $tableCount += 1;
  4240. if ($tableCount == $maxTables) {
  4241. break;
  4242. }
  4243. }
  4244. array_push($joinPaths, implode(',', $joinPath));
  4245. if ($tableCount == $maxTables) {
  4246. break;
  4247. }
  4248. }
  4249. $params['join'] = $joinPaths;
  4250. $request = RequestUtils::setParams($request, $params);
  4251. VariableStore::set("joinLimits.maxRecords", $maxRecords);
  4252. }
  4253. return $next->handle($request);
  4254. }
  4255. }
  4256. // file: src/Tqdev/PhpCrudApi/Middleware/JwtAuthMiddleware.php
  4257. class JwtAuthMiddleware extends Middleware
  4258. {
  4259. private function getVerifiedClaims(string $token, int $time, int $leeway, int $ttl, string $secret, array $requirements): array
  4260. {
  4261. $algorithms = array(
  4262. 'HS256' => 'sha256',
  4263. 'HS384' => 'sha384',
  4264. 'HS512' => 'sha512',
  4265. 'RS256' => 'sha256',
  4266. 'RS384' => 'sha384',
  4267. 'RS512' => 'sha512',
  4268. );
  4269. $token = explode('.', $token);
  4270. if (count($token) < 3) {
  4271. return array();
  4272. }
  4273. $header = json_decode(base64_decode(strtr($token[0], '-_', '+/')), true);
  4274. if (!$secret) {
  4275. return array();
  4276. }
  4277. if ($header['typ'] != 'JWT') {
  4278. return array();
  4279. }
  4280. $algorithm = $header['alg'];
  4281. if (!isset($algorithms[$algorithm])) {
  4282. return array();
  4283. }
  4284. if (!empty($requirements['alg']) && !in_array($algorithm, $requirements['alg'])) {
  4285. return array();
  4286. }
  4287. $hmac = $algorithms[$algorithm];
  4288. $signature = base64_decode(strtr($token[2], '-_', '+/'));
  4289. $data = "$token[0].$token[1]";
  4290. switch ($algorithm[0]) {
  4291. case 'H':
  4292. $hash = hash_hmac($hmac, $data, $secret, true);
  4293. if (function_exists('hash_equals')) {
  4294. $equals = hash_equals($signature, $hash);
  4295. } else {
  4296. $equals = $signature == $hash;
  4297. }
  4298. if (!$equals) {
  4299. return array();
  4300. }
  4301. break;
  4302. case 'R':
  4303. $equals = openssl_verify($data, $signature, $secret, $hmac) == 1;
  4304. if (!$equals) {
  4305. return array();
  4306. }
  4307. break;
  4308. }
  4309. $claims = json_decode(base64_decode(strtr($token[1], '-_', '+/')), true);
  4310. if (!$claims) {
  4311. return array();
  4312. }
  4313. foreach ($requirements as $field => $values) {
  4314. if (!empty($values)) {
  4315. if ($field != 'alg') {
  4316. if (!isset($claims[$field]) || !in_array($claims[$field], $values)) {
  4317. return array();
  4318. }
  4319. }
  4320. }
  4321. }
  4322. if (isset($claims['nbf']) && $time + $leeway < $claims['nbf']) {
  4323. return array();
  4324. }
  4325. if (isset($claims['iat']) && $time + $leeway < $claims['iat']) {
  4326. return array();
  4327. }
  4328. if (isset($claims['exp']) && $time - $leeway > $claims['exp']) {
  4329. return array();
  4330. }
  4331. if (isset($claims['iat']) && !isset($claims['exp'])) {
  4332. if ($time - $leeway > $claims['iat'] + $ttl) {
  4333. return array();
  4334. }
  4335. }
  4336. return $claims;
  4337. }
  4338. private function getClaims(string $token): array
  4339. {
  4340. $time = (int) $this->getProperty('time', time());
  4341. $leeway = (int) $this->getProperty('leeway', '5');
  4342. $ttl = (int) $this->getProperty('ttl', '30');
  4343. $secret = $this->getProperty('secret', '');
  4344. $requirements = array(
  4345. 'alg' => $this->getArrayProperty('algorithms', ''),
  4346. 'aud' => $this->getArrayProperty('audiences', ''),
  4347. 'iss' => $this->getArrayProperty('issuers', ''),
  4348. );
  4349. if (!$secret) {
  4350. return array();
  4351. }
  4352. return $this->getVerifiedClaims($token, $time, $leeway, $ttl, $secret, $requirements);
  4353. }
  4354. private function getAuthorizationToken(ServerRequestInterface $request): string
  4355. {
  4356. $headerName = $this->getProperty('header', 'X-Authorization');
  4357. $headerValue = RequestUtils::getHeader($request, $headerName);
  4358. $parts = explode(' ', trim($headerValue), 2);
  4359. if (count($parts) != 2) {
  4360. return '';
  4361. }
  4362. if ($parts[0] != 'Bearer') {
  4363. return '';
  4364. }
  4365. return $parts[1];
  4366. }
  4367. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4368. {
  4369. if (session_status() == PHP_SESSION_NONE) {
  4370. session_start();
  4371. }
  4372. $token = $this->getAuthorizationToken($request);
  4373. if ($token) {
  4374. $claims = $this->getClaims($token);
  4375. $_SESSION['claims'] = $claims;
  4376. if (empty($claims)) {
  4377. return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, 'JWT');
  4378. }
  4379. if (!headers_sent()) {
  4380. session_regenerate_id();
  4381. }
  4382. }
  4383. if (empty($_SESSION['claims'])) {
  4384. $authenticationMode = $this->getProperty('mode', 'required');
  4385. if ($authenticationMode == 'required') {
  4386. return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
  4387. }
  4388. }
  4389. return $next->handle($request);
  4390. }
  4391. }
  4392. // file: src/Tqdev/PhpCrudApi/Middleware/MultiTenancyMiddleware.php
  4393. class MultiTenancyMiddleware extends Middleware
  4394. {
  4395. private $reflection;
  4396. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4397. {
  4398. parent::__construct($router, $responder, $properties);
  4399. $this->reflection = $reflection;
  4400. }
  4401. private function getCondition(string $tableName, array $pairs): Condition
  4402. {
  4403. $condition = new NoCondition();
  4404. $table = $this->reflection->getTable($tableName);
  4405. foreach ($pairs as $k => $v) {
  4406. $condition = $condition->_and(new ColumnCondition($table->getColumn($k), 'eq', $v));
  4407. }
  4408. return $condition;
  4409. }
  4410. private function getPairs($handler, string $operation, string $tableName): array
  4411. {
  4412. $result = array();
  4413. $pairs = call_user_func($handler, $operation, $tableName);
  4414. $table = $this->reflection->getTable($tableName);
  4415. foreach ($pairs as $k => $v) {
  4416. if ($table->hasColumn($k)) {
  4417. $result[$k] = $v;
  4418. }
  4419. }
  4420. return $result;
  4421. }
  4422. private function handleRecord(ServerRequestInterface $request, string $operation, array $pairs): ServerRequestInterface
  4423. {
  4424. $record = $request->getParsedBody();
  4425. if ($record === null) {
  4426. return $request;
  4427. }
  4428. $multi = is_array($record);
  4429. $records = $multi ? $record : [$record];
  4430. foreach ($records as &$record) {
  4431. foreach ($pairs as $column => $value) {
  4432. if ($operation == 'create') {
  4433. $record->$column = $value;
  4434. } else {
  4435. if (isset($record->$column)) {
  4436. unset($record->$column);
  4437. }
  4438. }
  4439. }
  4440. }
  4441. return $request->withParsedBody($multi ? $records : $records[0]);
  4442. }
  4443. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4444. {
  4445. $handler = $this->getProperty('handler', '');
  4446. if ($handler !== '') {
  4447. $path = RequestUtils::getPathSegment($request, 1);
  4448. if ($path == 'records') {
  4449. $operation = RequestUtils::getOperation($request);
  4450. $tableNames = RequestUtils::getTableNames($request, $this->reflection);
  4451. foreach ($tableNames as $i => $tableName) {
  4452. if (!$this->reflection->hasTable($tableName)) {
  4453. continue;
  4454. }
  4455. $pairs = $this->getPairs($handler, $operation, $tableName);
  4456. if ($i == 0) {
  4457. if (in_array($operation, ['create', 'update', 'increment'])) {
  4458. $request = $this->handleRecord($request, $operation, $pairs);
  4459. }
  4460. }
  4461. $condition = $this->getCondition($tableName, $pairs);
  4462. VariableStore::set("multiTenancy.conditions.$tableName", $condition);
  4463. }
  4464. }
  4465. }
  4466. return $next->handle($request);
  4467. }
  4468. }
  4469. // file: src/Tqdev/PhpCrudApi/Middleware/PageLimitsMiddleware.php
  4470. class PageLimitsMiddleware extends Middleware
  4471. {
  4472. private $reflection;
  4473. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4474. {
  4475. parent::__construct($router, $responder, $properties);
  4476. $this->reflection = $reflection;
  4477. }
  4478. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4479. {
  4480. $operation = RequestUtils::getOperation($request);
  4481. if ($operation == 'list') {
  4482. $params = RequestUtils::getParams($request);
  4483. $maxPage = (int) $this->getProperty('pages', '100');
  4484. if (isset($params['page']) && $params['page'] && $maxPage > 0) {
  4485. if (strpos($params['page'][0], ',') === false) {
  4486. $page = $params['page'][0];
  4487. } else {
  4488. list($page, $size) = explode(',', $params['page'][0], 2);
  4489. }
  4490. if ($page > $maxPage) {
  4491. return $this->responder->error(ErrorCode::PAGINATION_FORBIDDEN, '');
  4492. }
  4493. }
  4494. $maxSize = (int) $this->getProperty('records', '1000');
  4495. if (!isset($params['size']) || !$params['size'] && $maxSize > 0) {
  4496. $params['size'] = array($maxSize);
  4497. } else {
  4498. $params['size'] = array(min($params['size'][0], $maxSize));
  4499. }
  4500. $request = RequestUtils::setParams($request, $params);
  4501. }
  4502. return $next->handle($request);
  4503. }
  4504. }
  4505. // file: src/Tqdev/PhpCrudApi/Middleware/SanitationMiddleware.php
  4506. class SanitationMiddleware extends Middleware
  4507. {
  4508. private $reflection;
  4509. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4510. {
  4511. parent::__construct($router, $responder, $properties);
  4512. $this->reflection = $reflection;
  4513. }
  4514. private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: object */
  4515. {
  4516. $context = (array) $record;
  4517. $tableName = $table->getName();
  4518. foreach ($context as $columnName => &$value) {
  4519. if ($table->hasColumn($columnName)) {
  4520. $column = $table->getColumn($columnName);
  4521. $value = call_user_func($handler, $operation, $tableName, $column->serialize(), $value);
  4522. }
  4523. }
  4524. return (object) $context;
  4525. }
  4526. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4527. {
  4528. $operation = RequestUtils::getOperation($request);
  4529. if (in_array($operation, ['create', 'update', 'increment'])) {
  4530. $tableName = RequestUtils::getPathSegment($request, 2);
  4531. if ($this->reflection->hasTable($tableName)) {
  4532. $record = $request->getParsedBody();
  4533. if ($record !== null) {
  4534. $handler = $this->getProperty('handler', '');
  4535. if ($handler !== '') {
  4536. $table = $this->reflection->getTable($tableName);
  4537. if (is_array($record)) {
  4538. foreach ($record as &$r) {
  4539. $r = $this->callHandler($handler, $r, $operation, $table);
  4540. }
  4541. } else {
  4542. $record = $this->callHandler($handler, $record, $operation, $table);
  4543. }
  4544. $request = $request->withParsedBody($record);
  4545. }
  4546. }
  4547. }
  4548. }
  4549. return $next->handle($request);
  4550. }
  4551. }
  4552. // file: src/Tqdev/PhpCrudApi/Middleware/ValidationMiddleware.php
  4553. class ValidationMiddleware extends Middleware
  4554. {
  4555. private $reflection;
  4556. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4557. {
  4558. parent::__construct($router, $responder, $properties);
  4559. $this->reflection = $reflection;
  4560. }
  4561. private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: ResponseInterface?*/
  4562. {
  4563. $context = (array) $record;
  4564. $details = array();
  4565. $tableName = $table->getName();
  4566. foreach ($context as $columnName => $value) {
  4567. if ($table->hasColumn($columnName)) {
  4568. $column = $table->getColumn($columnName);
  4569. $valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
  4570. if ($valid !== true && $valid !== '') {
  4571. $details[$columnName] = $valid;
  4572. }
  4573. }
  4574. }
  4575. if (count($details) > 0) {
  4576. return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
  4577. }
  4578. return null;
  4579. }
  4580. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4581. {
  4582. $operation = RequestUtils::getOperation($request);
  4583. if (in_array($operation, ['create', 'update', 'increment'])) {
  4584. $tableName = RequestUtils::getPathSegment($request, 2);
  4585. if ($this->reflection->hasTable($tableName)) {
  4586. $record = $request->getParsedBody();
  4587. if ($record !== null) {
  4588. $handler = $this->getProperty('handler', '');
  4589. if ($handler !== '') {
  4590. $table = $this->reflection->getTable($tableName);
  4591. if (is_array($record)) {
  4592. foreach ($record as $r) {
  4593. $response = $this->callHandler($handler, $r, $operation, $table);
  4594. if ($response !== null) {
  4595. return $response;
  4596. }
  4597. }
  4598. } else {
  4599. $response = $this->callHandler($handler, $record, $operation, $table);
  4600. if ($response !== null) {
  4601. return $response;
  4602. }
  4603. }
  4604. }
  4605. }
  4606. }
  4607. }
  4608. return $next->handle($request);
  4609. }
  4610. }
  4611. // file: src/Tqdev/PhpCrudApi/Middleware/XsrfMiddleware.php
  4612. class XsrfMiddleware extends Middleware
  4613. {
  4614. private function getToken(): string
  4615. {
  4616. $cookieName = $this->getProperty('cookieName', 'XSRF-TOKEN');
  4617. if (isset($_COOKIE[$cookieName])) {
  4618. $token = $_COOKIE[$cookieName];
  4619. } else {
  4620. $secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
  4621. $token = bin2hex(random_bytes(8));
  4622. if (!headers_sent()) {
  4623. setcookie($cookieName, $token, 0, '', '', $secure);
  4624. }
  4625. }
  4626. return $token;
  4627. }
  4628. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4629. {
  4630. $token = $this->getToken();
  4631. $method = $request->getMethod();
  4632. $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
  4633. if (!in_array($method, $excludeMethods)) {
  4634. $headerName = $this->getProperty('headerName', 'X-XSRF-TOKEN');
  4635. if ($token != $request->getHeader($headerName)) {
  4636. return $this->responder->error(ErrorCode::BAD_OR_MISSING_XSRF_TOKEN, '');
  4637. }
  4638. }
  4639. return $next->handle($request);
  4640. }
  4641. }
  4642. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiBuilder.php
  4643. class OpenApiBuilder
  4644. {
  4645. private $openapi;
  4646. private $reflection;
  4647. private $operations = [
  4648. 'list' => 'get',
  4649. 'create' => 'post',
  4650. 'read' => 'get',
  4651. 'update' => 'put',
  4652. 'delete' => 'delete',
  4653. 'increment' => 'patch',
  4654. ];
  4655. private $types = [
  4656. 'integer' => ['type' => 'integer', 'format' => 'int32'],
  4657. 'bigint' => ['type' => 'integer', 'format' => 'int64'],
  4658. 'varchar' => ['type' => 'string'],
  4659. 'clob' => ['type' => 'string'],
  4660. 'varbinary' => ['type' => 'string', 'format' => 'byte'],
  4661. 'blob' => ['type' => 'string', 'format' => 'byte'],
  4662. 'decimal' => ['type' => 'string'],
  4663. 'float' => ['type' => 'number', 'format' => 'float'],
  4664. 'double' => ['type' => 'number', 'format' => 'double'],
  4665. 'date' => ['type' => 'string', 'format' => 'date'],
  4666. 'time' => ['type' => 'string', 'format' => 'date-time'],
  4667. 'timestamp' => ['type' => 'string', 'format' => 'date-time'],
  4668. 'geometry' => ['type' => 'string'],
  4669. 'boolean' => ['type' => 'boolean'],
  4670. ];
  4671. public function __construct(ReflectionService $reflection, $base)
  4672. {
  4673. $this->reflection = $reflection;
  4674. $this->openapi = new OpenApiDefinition($base);
  4675. }
  4676. private function getServerUrl(): string
  4677. {
  4678. $protocol = @$_SERVER['HTTP_X_FORWARDED_PROTO'] ?: @$_SERVER['REQUEST_SCHEME'] ?: ((isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") ? "https" : "http");
  4679. $port = @intval($_SERVER['HTTP_X_FORWARDED_PORT']) ?: @intval($_SERVER["SERVER_PORT"]) ?: (($protocol === 'https') ? 443 : 80);
  4680. $host = @explode(":", $_SERVER['HTTP_HOST'])[0] ?: @$_SERVER['SERVER_NAME'] ?: @$_SERVER['SERVER_ADDR'];
  4681. $port = ($protocol === 'https' && $port === 443) || ($protocol === 'http' && $port === 80) ? '' : ':' . $port;
  4682. $path = @trim(substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '/openapi')), '/');
  4683. return sprintf('%s://%s%s/%s', $protocol, $host, $port, $path);
  4684. }
  4685. private function getAllTableReferences(): array
  4686. {
  4687. $tableReferences = array();
  4688. foreach ($this->reflection->getTableNames() as $tableName) {
  4689. $table = $this->reflection->getTable($tableName);
  4690. foreach ($table->getColumnNames() as $columnName) {
  4691. $column = $table->getColumn($columnName);
  4692. $referencedTableName = $column->getFk();
  4693. if ($referencedTableName) {
  4694. if (!isset($tableReferences[$referencedTableName])) {
  4695. $tableReferences[$referencedTableName] = array();
  4696. }
  4697. $tableReferences[$referencedTableName][] = "$tableName.$columnName";
  4698. }
  4699. }
  4700. }
  4701. return $tableReferences;
  4702. }
  4703. public function build(): OpenApiDefinition
  4704. {
  4705. $this->openapi->set("openapi", "3.0.0");
  4706. if (!$this->openapi->has("servers") && isset($_SERVER['REQUEST_URI'])) {
  4707. $this->openapi->set("servers|0|url", $this->getServerUrl());
  4708. }
  4709. $tableNames = $this->reflection->getTableNames();
  4710. foreach ($tableNames as $tableName) {
  4711. $this->setPath($tableName);
  4712. }
  4713. $this->openapi->set("components|responses|pk_integer|description", "inserted primary key value (integer)");
  4714. $this->openapi->set("components|responses|pk_integer|content|application/json|schema|type", "integer");
  4715. $this->openapi->set("components|responses|pk_integer|content|application/json|schema|format", "int64");
  4716. $this->openapi->set("components|responses|pk_string|description", "inserted primary key value (string)");
  4717. $this->openapi->set("components|responses|pk_string|content|application/json|schema|type", "string");
  4718. $this->openapi->set("components|responses|pk_string|content|application/json|schema|format", "uuid");
  4719. $this->openapi->set("components|responses|rows_affected|description", "number of rows affected (integer)");
  4720. $this->openapi->set("components|responses|rows_affected|content|application/json|schema|type", "integer");
  4721. $this->openapi->set("components|responses|rows_affected|content|application/json|schema|format", "int64");
  4722. $tableReferences = $this->getAllTableReferences();
  4723. foreach ($tableNames as $tableName) {
  4724. $references = isset($tableReferences[$tableName]) ? $tableReferences[$tableName] : array();
  4725. $this->setComponentSchema($tableName, $references);
  4726. $this->setComponentResponse($tableName);
  4727. $this->setComponentRequestBody($tableName);
  4728. }
  4729. $this->setComponentParameters();
  4730. foreach ($tableNames as $index => $tableName) {
  4731. $this->setTag($index, $tableName);
  4732. }
  4733. return $this->openapi;
  4734. }
  4735. private function isOperationOnTableAllowed(string $operation, string $tableName): bool
  4736. {
  4737. $tableHandler = VariableStore::get('authorization.tableHandler');
  4738. if (!$tableHandler) {
  4739. return true;
  4740. }
  4741. return (bool) call_user_func($tableHandler, $operation, $tableName);
  4742. }
  4743. private function isOperationOnColumnAllowed(string $operation, string $tableName, string $columnName): bool
  4744. {
  4745. $columnHandler = VariableStore::get('authorization.columnHandler');
  4746. if (!$columnHandler) {
  4747. return true;
  4748. }
  4749. return (bool) call_user_func($columnHandler, $operation, $tableName, $columnName);
  4750. }
  4751. private function setPath(string $tableName) /*: void*/
  4752. {
  4753. $table = $this->reflection->getTable($tableName);
  4754. $type = $table->getType();
  4755. $pk = $table->getPk();
  4756. $pkName = $pk ? $pk->getName() : '';
  4757. foreach ($this->operations as $operation => $method) {
  4758. if (!$pkName && $operation != 'list') {
  4759. continue;
  4760. }
  4761. if ($type != 'table' && $operation != 'list') {
  4762. continue;
  4763. }
  4764. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4765. continue;
  4766. }
  4767. $parameters = [];
  4768. if (in_array($operation, ['list', 'create'])) {
  4769. $path = sprintf('/records/%s', $tableName);
  4770. if ($operation == 'list') {
  4771. $parameters = ['filter', 'include', 'exclude', 'order', 'size', 'page', 'join'];
  4772. }
  4773. } else {
  4774. $path = sprintf('/records/%s/{%s}', $tableName, $pkName);
  4775. if ($operation == 'read') {
  4776. $parameters = ['pk', 'include', 'exclude', 'join'];
  4777. } else {
  4778. $parameters = ['pk'];
  4779. }
  4780. }
  4781. foreach ($parameters as $p => $parameter) {
  4782. $this->openapi->set("paths|$path|$method|parameters|$p|\$ref", "#/components/parameters/$parameter");
  4783. }
  4784. if (in_array($operation, ['create', 'update', 'increment'])) {
  4785. $this->openapi->set("paths|$path|$method|requestBody|\$ref", "#/components/requestBodies/$operation-" . urlencode($tableName));
  4786. }
  4787. $this->openapi->set("paths|$path|$method|tags|0", "$tableName");
  4788. $this->openapi->set("paths|$path|$method|description", "$operation $tableName");
  4789. switch ($operation) {
  4790. case 'list':
  4791. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . urlencode($tableName));
  4792. break;
  4793. case 'create':
  4794. if ($pk->getType() == 'integer') {
  4795. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_integer");
  4796. } else {
  4797. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_string");
  4798. }
  4799. break;
  4800. case 'read':
  4801. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . urlencode($tableName));
  4802. break;
  4803. case 'update':
  4804. case 'delete':
  4805. case 'increment':
  4806. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/rows_affected");
  4807. break;
  4808. }
  4809. }
  4810. }
  4811. private function setComponentSchema(string $tableName, array $references) /*: void*/
  4812. {
  4813. $table = $this->reflection->getTable($tableName);
  4814. $type = $table->getType();
  4815. $pk = $table->getPk();
  4816. $pkName = $pk ? $pk->getName() : '';
  4817. foreach ($this->operations as $operation => $method) {
  4818. if (!$pkName && $operation != 'list') {
  4819. continue;
  4820. }
  4821. if ($type != 'table' && $operation != 'list') {
  4822. continue;
  4823. }
  4824. if ($operation == 'delete') {
  4825. continue;
  4826. }
  4827. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4828. continue;
  4829. }
  4830. if ($operation == 'list') {
  4831. $this->openapi->set("components|schemas|$operation-$tableName|type", "object");
  4832. $this->openapi->set("components|schemas|$operation-$tableName|properties|results|type", "integer");
  4833. $this->openapi->set("components|schemas|$operation-$tableName|properties|results|format", "int64");
  4834. $this->openapi->set("components|schemas|$operation-$tableName|properties|records|type", "array");
  4835. $prefix = "components|schemas|$operation-$tableName|properties|records|items";
  4836. } else {
  4837. $prefix = "components|schemas|$operation-$tableName";
  4838. }
  4839. $this->openapi->set("$prefix|type", "object");
  4840. foreach ($table->getColumnNames() as $columnName) {
  4841. if (!$this->isOperationOnColumnAllowed($operation, $tableName, $columnName)) {
  4842. continue;
  4843. }
  4844. $column = $table->getColumn($columnName);
  4845. $properties = $this->types[$column->getType()];
  4846. foreach ($properties as $key => $value) {
  4847. $this->openapi->set("$prefix|properties|$columnName|$key", $value);
  4848. }
  4849. if ($column->getPk()) {
  4850. $this->openapi->set("$prefix|properties|$columnName|x-primary-key", true);
  4851. $this->openapi->set("$prefix|properties|$columnName|x-referenced", $references);
  4852. }
  4853. $fk = $column->getFk();
  4854. if ($fk) {
  4855. $this->openapi->set("$prefix|properties|$columnName|x-references", $fk);
  4856. }
  4857. }
  4858. }
  4859. }
  4860. private function setComponentResponse(string $tableName) /*: void*/
  4861. {
  4862. $table = $this->reflection->getTable($tableName);
  4863. $type = $table->getType();
  4864. $pk = $table->getPk();
  4865. $pkName = $pk ? $pk->getName() : '';
  4866. foreach (['list', 'read'] as $operation) {
  4867. if (!$pkName && $operation != 'list') {
  4868. continue;
  4869. }
  4870. if ($type != 'table' && $operation != 'list') {
  4871. continue;
  4872. }
  4873. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4874. continue;
  4875. }
  4876. if ($operation == 'list') {
  4877. $this->openapi->set("components|responses|$operation-$tableName|description", "list of $tableName records");
  4878. } else {
  4879. $this->openapi->set("components|responses|$operation-$tableName|description", "single $tableName record");
  4880. }
  4881. $this->openapi->set("components|responses|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . urlencode($tableName));
  4882. }
  4883. }
  4884. private function setComponentRequestBody(string $tableName) /*: void*/
  4885. {
  4886. $table = $this->reflection->getTable($tableName);
  4887. $type = $table->getType();
  4888. $pk = $table->getPk();
  4889. $pkName = $pk ? $pk->getName() : '';
  4890. if ($pkName && $type == 'table') {
  4891. foreach (['create', 'update', 'increment'] as $operation) {
  4892. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4893. continue;
  4894. }
  4895. $this->openapi->set("components|requestBodies|$operation-$tableName|description", "single $tableName record");
  4896. $this->openapi->set("components|requestBodies|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . urlencode($tableName));
  4897. }
  4898. }
  4899. }
  4900. private function setComponentParameters() /*: void*/
  4901. {
  4902. $this->openapi->set("components|parameters|pk|name", "id");
  4903. $this->openapi->set("components|parameters|pk|in", "path");
  4904. $this->openapi->set("components|parameters|pk|schema|type", "string");
  4905. $this->openapi->set("components|parameters|pk|description", "primary key value");
  4906. $this->openapi->set("components|parameters|pk|required", true);
  4907. $this->openapi->set("components|parameters|filter|name", "filter");
  4908. $this->openapi->set("components|parameters|filter|in", "query");
  4909. $this->openapi->set("components|parameters|filter|schema|type", "array");
  4910. $this->openapi->set("components|parameters|filter|schema|items|type", "string");
  4911. $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");
  4912. $this->openapi->set("components|parameters|filter|required", false);
  4913. $this->openapi->set("components|parameters|include|name", "include");
  4914. $this->openapi->set("components|parameters|include|in", "query");
  4915. $this->openapi->set("components|parameters|include|schema|type", "string");
  4916. $this->openapi->set("components|parameters|include|description", "Columns you want to include in the output (comma separated). Example: posts.*,categories.name");
  4917. $this->openapi->set("components|parameters|include|required", false);
  4918. $this->openapi->set("components|parameters|exclude|name", "exclude");
  4919. $this->openapi->set("components|parameters|exclude|in", "query");
  4920. $this->openapi->set("components|parameters|exclude|schema|type", "string");
  4921. $this->openapi->set("components|parameters|exclude|description", "Columns you want to exclude from the output (comma separated). Example: posts.content");
  4922. $this->openapi->set("components|parameters|exclude|required", false);
  4923. $this->openapi->set("components|parameters|order|name", "order");
  4924. $this->openapi->set("components|parameters|order|in", "query");
  4925. $this->openapi->set("components|parameters|order|schema|type", "array");
  4926. $this->openapi->set("components|parameters|order|schema|items|type", "string");
  4927. $this->openapi->set("components|parameters|order|description", "Column you want to sort on and the sort direction (comma separated). Example: id,desc");
  4928. $this->openapi->set("components|parameters|order|required", false);
  4929. $this->openapi->set("components|parameters|size|name", "size");
  4930. $this->openapi->set("components|parameters|size|in", "query");
  4931. $this->openapi->set("components|parameters|size|schema|type", "string");
  4932. $this->openapi->set("components|parameters|size|description", "Maximum number of results (for top lists). Example: 10");
  4933. $this->openapi->set("components|parameters|size|required", false);
  4934. $this->openapi->set("components|parameters|page|name", "page");
  4935. $this->openapi->set("components|parameters|page|in", "query");
  4936. $this->openapi->set("components|parameters|page|schema|type", "string");
  4937. $this->openapi->set("components|parameters|page|description", "Page number and page size (comma separated). Example: 1,10");
  4938. $this->openapi->set("components|parameters|page|required", false);
  4939. $this->openapi->set("components|parameters|join|name", "join");
  4940. $this->openapi->set("components|parameters|join|in", "query");
  4941. $this->openapi->set("components|parameters|join|schema|type", "array");
  4942. $this->openapi->set("components|parameters|join|schema|items|type", "string");
  4943. $this->openapi->set("components|parameters|join|description", "Paths (comma separated) to related entities that you want to include. Example: comments,users");
  4944. $this->openapi->set("components|parameters|join|required", false);
  4945. }
  4946. private function setTag(int $index, string $tableName) /*: void*/
  4947. {
  4948. $this->openapi->set("tags|$index|name", "$tableName");
  4949. $this->openapi->set("tags|$index|description", "$tableName operations");
  4950. }
  4951. }
  4952. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiDefinition.php
  4953. class OpenApiDefinition implements \JsonSerializable
  4954. {
  4955. private $root;
  4956. public function __construct($base)
  4957. {
  4958. $this->root = $base;
  4959. }
  4960. public function set(string $path, $value) /*: void*/
  4961. {
  4962. $parts = explode('|', trim($path, '|'));
  4963. $current = &$this->root;
  4964. while (count($parts) > 0) {
  4965. $part = array_shift($parts);
  4966. if (!isset($current[$part])) {
  4967. $current[$part] = [];
  4968. }
  4969. $current = &$current[$part];
  4970. }
  4971. $current = $value;
  4972. }
  4973. public function has(string $path): bool
  4974. {
  4975. $parts = explode('|', trim($path, '|'));
  4976. $current = &$this->root;
  4977. while (count($parts) > 0) {
  4978. $part = array_shift($parts);
  4979. if (!isset($current[$part])) {
  4980. return false;
  4981. }
  4982. $current = &$current[$part];
  4983. }
  4984. return true;
  4985. }
  4986. public function jsonSerialize()
  4987. {
  4988. return $this->root;
  4989. }
  4990. }
  4991. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiService.php
  4992. class OpenApiService
  4993. {
  4994. private $builder;
  4995. public function __construct(ReflectionService $reflection, array $base)
  4996. {
  4997. $this->builder = new OpenApiBuilder($reflection, $base);
  4998. }
  4999. public function get(): OpenApiDefinition
  5000. {
  5001. return $this->builder->build();
  5002. }
  5003. }
  5004. // file: src/Tqdev/PhpCrudApi/Record/Condition/AndCondition.php
  5005. class AndCondition extends Condition
  5006. {
  5007. private $conditions;
  5008. public function __construct(Condition $condition1, Condition $condition2)
  5009. {
  5010. $this->conditions = [$condition1, $condition2];
  5011. }
  5012. public function _and(Condition $condition): Condition
  5013. {
  5014. if ($condition instanceof NoCondition) {
  5015. return $this;
  5016. }
  5017. $this->conditions[] = $condition;
  5018. return $this;
  5019. }
  5020. public function getConditions(): array
  5021. {
  5022. return $this->conditions;
  5023. }
  5024. public static function fromArray(array $conditions): Condition
  5025. {
  5026. $condition = new NoCondition();
  5027. foreach ($conditions as $c) {
  5028. $condition = $condition->_and($c);
  5029. }
  5030. return $condition;
  5031. }
  5032. }
  5033. // file: src/Tqdev/PhpCrudApi/Record/Condition/ColumnCondition.php
  5034. class ColumnCondition extends Condition
  5035. {
  5036. private $column;
  5037. private $operator;
  5038. private $value;
  5039. public function __construct(ReflectedColumn $column, string $operator, string $value)
  5040. {
  5041. $this->column = $column;
  5042. $this->operator = $operator;
  5043. $this->value = $value;
  5044. }
  5045. public function getColumn(): ReflectedColumn
  5046. {
  5047. return $this->column;
  5048. }
  5049. public function getOperator(): string
  5050. {
  5051. return $this->operator;
  5052. }
  5053. public function getValue(): string
  5054. {
  5055. return $this->value;
  5056. }
  5057. }
  5058. // file: src/Tqdev/PhpCrudApi/Record/Condition/Condition.php
  5059. abstract class Condition
  5060. {
  5061. public function _and(Condition $condition): Condition
  5062. {
  5063. if ($condition instanceof NoCondition) {
  5064. return $this;
  5065. }
  5066. return new AndCondition($this, $condition);
  5067. }
  5068. public function _or(Condition $condition): Condition
  5069. {
  5070. if ($condition instanceof NoCondition) {
  5071. return $this;
  5072. }
  5073. return new OrCondition($this, $condition);
  5074. }
  5075. public function _not(): Condition
  5076. {
  5077. return new NotCondition($this);
  5078. }
  5079. public static function fromString(ReflectedTable $table, string $value): Condition
  5080. {
  5081. $condition = new NoCondition();
  5082. $parts = explode(',', $value, 3);
  5083. if (count($parts) < 2) {
  5084. return $condition;
  5085. }
  5086. if (count($parts) < 3) {
  5087. $parts[2] = '';
  5088. }
  5089. $field = $table->getColumn($parts[0]);
  5090. $command = $parts[1];
  5091. $negate = false;
  5092. $spatial = false;
  5093. if (strlen($command) > 2) {
  5094. if (substr($command, 0, 1) == 'n') {
  5095. $negate = true;
  5096. $command = substr($command, 1);
  5097. }
  5098. if (substr($command, 0, 1) == 's') {
  5099. $spatial = true;
  5100. $command = substr($command, 1);
  5101. }
  5102. }
  5103. if ($spatial) {
  5104. if (in_array($command, ['co', 'cr', 'di', 'eq', 'in', 'ov', 'to', 'wi', 'ic', 'is', 'iv'])) {
  5105. $condition = new SpatialCondition($field, $command, $parts[2]);
  5106. }
  5107. } else {
  5108. if (in_array($command, ['cs', 'sw', 'ew', 'eq', 'lt', 'le', 'ge', 'gt', 'bt', 'in', 'is'])) {
  5109. $condition = new ColumnCondition($field, $command, $parts[2]);
  5110. }
  5111. }
  5112. if ($negate) {
  5113. $condition = $condition->_not();
  5114. }
  5115. return $condition;
  5116. }
  5117. }
  5118. // file: src/Tqdev/PhpCrudApi/Record/Condition/NoCondition.php
  5119. class NoCondition extends Condition
  5120. {
  5121. public function _and(Condition $condition): Condition
  5122. {
  5123. return $condition;
  5124. }
  5125. public function _or(Condition $condition): Condition
  5126. {
  5127. return $condition;
  5128. }
  5129. public function _not(): Condition
  5130. {
  5131. return $this;
  5132. }
  5133. }
  5134. // file: src/Tqdev/PhpCrudApi/Record/Condition/NotCondition.php
  5135. class NotCondition extends Condition
  5136. {
  5137. private $condition;
  5138. public function __construct(Condition $condition)
  5139. {
  5140. $this->condition = $condition;
  5141. }
  5142. public function getCondition(): Condition
  5143. {
  5144. return $this->condition;
  5145. }
  5146. }
  5147. // file: src/Tqdev/PhpCrudApi/Record/Condition/OrCondition.php
  5148. class OrCondition extends Condition
  5149. {
  5150. private $conditions;
  5151. public function __construct(Condition $condition1, Condition $condition2)
  5152. {
  5153. $this->conditions = [$condition1, $condition2];
  5154. }
  5155. public function _or(Condition $condition): Condition
  5156. {
  5157. if ($condition instanceof NoCondition) {
  5158. return $this;
  5159. }
  5160. $this->conditions[] = $condition;
  5161. return $this;
  5162. }
  5163. public function getConditions(): array
  5164. {
  5165. return $this->conditions;
  5166. }
  5167. public static function fromArray(array $conditions): Condition
  5168. {
  5169. $condition = new NoCondition();
  5170. foreach ($conditions as $c) {
  5171. $condition = $condition->_or($c);
  5172. }
  5173. return $condition;
  5174. }
  5175. }
  5176. // file: src/Tqdev/PhpCrudApi/Record/Condition/SpatialCondition.php
  5177. class SpatialCondition extends ColumnCondition
  5178. {
  5179. }
  5180. // file: src/Tqdev/PhpCrudApi/Record/Document/ErrorDocument.php
  5181. class ErrorDocument implements \JsonSerializable
  5182. {
  5183. public $code;
  5184. public $message;
  5185. public $details;
  5186. public function __construct(ErrorCode $errorCode, string $argument, $details)
  5187. {
  5188. $this->code = $errorCode->getCode();
  5189. $this->message = $errorCode->getMessage($argument);
  5190. $this->details = $details;
  5191. }
  5192. public function getCode(): int
  5193. {
  5194. return $this->code;
  5195. }
  5196. public function getMessage(): string
  5197. {
  5198. return $this->message;
  5199. }
  5200. public function serialize()
  5201. {
  5202. return [
  5203. 'code' => $this->code,
  5204. 'message' => $this->message,
  5205. 'details' => $this->details,
  5206. ];
  5207. }
  5208. public function jsonSerialize()
  5209. {
  5210. return array_filter($this->serialize());
  5211. }
  5212. }
  5213. // file: src/Tqdev/PhpCrudApi/Record/Document/ListDocument.php
  5214. class ListDocument implements \JsonSerializable
  5215. {
  5216. private $records;
  5217. private $results;
  5218. public function __construct(array $records, int $results)
  5219. {
  5220. $this->records = $records;
  5221. $this->results = $results;
  5222. }
  5223. public function getRecords(): array
  5224. {
  5225. return $this->records;
  5226. }
  5227. public function getResults(): int
  5228. {
  5229. return $this->results;
  5230. }
  5231. public function serialize()
  5232. {
  5233. return [
  5234. 'records' => $this->records,
  5235. 'results' => $this->results,
  5236. ];
  5237. }
  5238. public function jsonSerialize()
  5239. {
  5240. return array_filter($this->serialize(), function ($v) {
  5241. return $v !== 0;
  5242. });
  5243. }
  5244. }
  5245. // file: src/Tqdev/PhpCrudApi/Record/ColumnIncluder.php
  5246. class ColumnIncluder
  5247. {
  5248. private function isMandatory(string $tableName, string $columnName, array $params): bool
  5249. {
  5250. return isset($params['mandatory']) && in_array($tableName . "." . $columnName, $params['mandatory']);
  5251. }
  5252. private function select(string $tableName, bool $primaryTable, array $params, string $paramName,
  5253. array $columnNames, bool $include): array{
  5254. if (!isset($params[$paramName])) {
  5255. return $columnNames;
  5256. }
  5257. $columns = array();
  5258. foreach (explode(',', $params[$paramName][0]) as $columnName) {
  5259. $columns[$columnName] = true;
  5260. }
  5261. $result = array();
  5262. foreach ($columnNames as $columnName) {
  5263. $match = isset($columns['*.*']);
  5264. if (!$match) {
  5265. $match = isset($columns[$tableName . '.*']) || isset($columns[$tableName . '.' . $columnName]);
  5266. }
  5267. if ($primaryTable && !$match) {
  5268. $match = isset($columns['*']) || isset($columns[$columnName]);
  5269. }
  5270. if ($match) {
  5271. if ($include || $this->isMandatory($tableName, $columnName, $params)) {
  5272. $result[] = $columnName;
  5273. }
  5274. } else {
  5275. if (!$include || $this->isMandatory($tableName, $columnName, $params)) {
  5276. $result[] = $columnName;
  5277. }
  5278. }
  5279. }
  5280. return $result;
  5281. }
  5282. public function getNames(ReflectedTable $table, bool $primaryTable, array $params): array
  5283. {
  5284. $tableName = $table->getName();
  5285. $results = $table->getColumnNames();
  5286. $results = $this->select($tableName, $primaryTable, $params, 'include', $results, true);
  5287. $results = $this->select($tableName, $primaryTable, $params, 'exclude', $results, false);
  5288. return $results;
  5289. }
  5290. public function getValues(ReflectedTable $table, bool $primaryTable, /* object */ $record, array $params): array
  5291. {
  5292. $results = array();
  5293. $columnNames = $this->getNames($table, $primaryTable, $params);
  5294. foreach ($columnNames as $columnName) {
  5295. if (property_exists($record, $columnName)) {
  5296. $results[$columnName] = $record->$columnName;
  5297. }
  5298. }
  5299. return $results;
  5300. }
  5301. }
  5302. // file: src/Tqdev/PhpCrudApi/Record/ErrorCode.php
  5303. class ErrorCode
  5304. {
  5305. private $code;
  5306. private $message;
  5307. private $status;
  5308. const ERROR_NOT_FOUND = 9999;
  5309. const ROUTE_NOT_FOUND = 1000;
  5310. const TABLE_NOT_FOUND = 1001;
  5311. const ARGUMENT_COUNT_MISMATCH = 1002;
  5312. const RECORD_NOT_FOUND = 1003;
  5313. const ORIGIN_FORBIDDEN = 1004;
  5314. const COLUMN_NOT_FOUND = 1005;
  5315. const TABLE_ALREADY_EXISTS = 1006;
  5316. const COLUMN_ALREADY_EXISTS = 1007;
  5317. const HTTP_MESSAGE_NOT_READABLE = 1008;
  5318. const DUPLICATE_KEY_EXCEPTION = 1009;
  5319. const DATA_INTEGRITY_VIOLATION = 1010;
  5320. const AUTHENTICATION_REQUIRED = 1011;
  5321. const AUTHENTICATION_FAILED = 1012;
  5322. const INPUT_VALIDATION_FAILED = 1013;
  5323. const OPERATION_FORBIDDEN = 1014;
  5324. const OPERATION_NOT_SUPPORTED = 1015;
  5325. const TEMPORARY_OR_PERMANENTLY_BLOCKED = 1016;
  5326. const BAD_OR_MISSING_XSRF_TOKEN = 1017;
  5327. const ONLY_AJAX_REQUESTS_ALLOWED = 1018;
  5328. const PAGINATION_FORBIDDEN = 1019;
  5329. private $values = [
  5330. 9999 => ["%s", ResponseFactory::INTERNAL_SERVER_ERROR],
  5331. 1000 => ["Route '%s' not found", ResponseFactory::NOT_FOUND],
  5332. 1001 => ["Table '%s' not found", ResponseFactory::NOT_FOUND],
  5333. 1002 => ["Argument count mismatch in '%s'", ResponseFactory::UNPROCESSABLE_ENTITY],
  5334. 1003 => ["Record '%s' not found", ResponseFactory::NOT_FOUND],
  5335. 1004 => ["Origin '%s' is forbidden", ResponseFactory::FORBIDDEN],
  5336. 1005 => ["Column '%s' not found", ResponseFactory::NOT_FOUND],
  5337. 1006 => ["Table '%s' already exists", ResponseFactory::CONFLICT],
  5338. 1007 => ["Column '%s' already exists", ResponseFactory::CONFLICT],
  5339. 1008 => ["Cannot read HTTP message", ResponseFactory::UNPROCESSABLE_ENTITY],
  5340. 1009 => ["Duplicate key exception", ResponseFactory::CONFLICT],
  5341. 1010 => ["Data integrity violation", ResponseFactory::CONFLICT],
  5342. 1011 => ["Authentication required", ResponseFactory::UNAUTHORIZED],
  5343. 1012 => ["Authentication failed for '%s'", ResponseFactory::FORBIDDEN],
  5344. 1013 => ["Input validation failed for '%s'", ResponseFactory::UNPROCESSABLE_ENTITY],
  5345. 1014 => ["Operation forbidden", ResponseFactory::FORBIDDEN],
  5346. 1015 => ["Operation '%s' not supported", ResponseFactory::METHOD_NOT_ALLOWED],
  5347. 1016 => ["Temporary or permanently blocked", ResponseFactory::FORBIDDEN],
  5348. 1017 => ["Bad or missing XSRF token", ResponseFactory::FORBIDDEN],
  5349. 1018 => ["Only AJAX requests allowed for '%s'", ResponseFactory::FORBIDDEN],
  5350. 1019 => ["Pagination forbidden", ResponseFactory::FORBIDDEN],
  5351. ];
  5352. public function __construct(int $code)
  5353. {
  5354. if (!isset($this->values[$code])) {
  5355. $code = 9999;
  5356. }
  5357. $this->code = $code;
  5358. $this->message = $this->values[$code][0];
  5359. $this->status = $this->values[$code][1];
  5360. }
  5361. public function getCode(): int
  5362. {
  5363. return $this->code;
  5364. }
  5365. public function getMessage(string $argument): string
  5366. {
  5367. return sprintf($this->message, $argument);
  5368. }
  5369. public function getStatus(): int
  5370. {
  5371. return $this->status;
  5372. }
  5373. }
  5374. // file: src/Tqdev/PhpCrudApi/Record/FilterInfo.php
  5375. class FilterInfo
  5376. {
  5377. private function addConditionFromFilterPath(PathTree $conditions, array $path, ReflectedTable $table, array $params)
  5378. {
  5379. $key = 'filter' . implode('', $path);
  5380. if (isset($params[$key])) {
  5381. foreach ($params[$key] as $filter) {
  5382. $condition = Condition::fromString($table, $filter);
  5383. if (($condition instanceof NoCondition) == false) {
  5384. $conditions->put($path, $condition);
  5385. }
  5386. }
  5387. }
  5388. }
  5389. private function getConditionsAsPathTree(ReflectedTable $table, array $params): PathTree
  5390. {
  5391. $conditions = new PathTree();
  5392. $this->addConditionFromFilterPath($conditions, [], $table, $params);
  5393. for ($n = ord('0'); $n <= ord('9'); $n++) {
  5394. $this->addConditionFromFilterPath($conditions, [chr($n)], $table, $params);
  5395. for ($l = ord('a'); $l <= ord('f'); $l++) {
  5396. $this->addConditionFromFilterPath($conditions, [chr($n), chr($l)], $table, $params);
  5397. }
  5398. }
  5399. return $conditions;
  5400. }
  5401. private function combinePathTreeOfConditions(PathTree $tree): Condition
  5402. {
  5403. $andConditions = $tree->getValues();
  5404. $and = AndCondition::fromArray($andConditions);
  5405. $orConditions = [];
  5406. foreach ($tree->getKeys() as $p) {
  5407. $orConditions[] = $this->combinePathTreeOfConditions($tree->get($p));
  5408. }
  5409. $or = OrCondition::fromArray($orConditions);
  5410. return $and->_and($or);
  5411. }
  5412. public function getCombinedConditions(ReflectedTable $table, array $params): Condition
  5413. {
  5414. return $this->combinePathTreeOfConditions($this->getConditionsAsPathTree($table, $params));
  5415. }
  5416. }
  5417. // file: src/Tqdev/PhpCrudApi/Record/HabtmValues.php
  5418. class HabtmValues
  5419. {
  5420. public $pkValues;
  5421. public $fkValues;
  5422. public function __construct(array $pkValues, array $fkValues)
  5423. {
  5424. $this->pkValues = $pkValues;
  5425. $this->fkValues = $fkValues;
  5426. }
  5427. }
  5428. // file: src/Tqdev/PhpCrudApi/Record/OrderingInfo.php
  5429. class OrderingInfo
  5430. {
  5431. public function getColumnOrdering(ReflectedTable $table, array $params): array
  5432. {
  5433. $fields = array();
  5434. if (isset($params['order'])) {
  5435. foreach ($params['order'] as $order) {
  5436. $parts = explode(',', $order, 3);
  5437. $columnName = $parts[0];
  5438. if (!$table->hasColumn($columnName)) {
  5439. continue;
  5440. }
  5441. $ascending = 'ASC';
  5442. if (count($parts) > 1) {
  5443. if (substr(strtoupper($parts[1]), 0, 4) == "DESC") {
  5444. $ascending = 'DESC';
  5445. }
  5446. }
  5447. $fields[] = [$columnName, $ascending];
  5448. }
  5449. }
  5450. if (count($fields) == 0) {
  5451. return $this->getDefaultColumnOrdering($table);
  5452. }
  5453. return $fields;
  5454. }
  5455. public function getDefaultColumnOrdering(ReflectedTable $table): array
  5456. {
  5457. $fields = array();
  5458. $pk = $table->getPk();
  5459. if ($pk) {
  5460. $fields[] = [$pk->getName(), 'ASC'];
  5461. } else {
  5462. foreach ($table->getColumnNames() as $columnName) {
  5463. $fields[] = [$columnName, 'ASC'];
  5464. }
  5465. }
  5466. return $fields;
  5467. }
  5468. }
  5469. // file: src/Tqdev/PhpCrudApi/Record/PaginationInfo.php
  5470. class PaginationInfo
  5471. {
  5472. public $DEFAULT_PAGE_SIZE = 20;
  5473. public function hasPage(array $params): bool
  5474. {
  5475. return isset($params['page']);
  5476. }
  5477. public function getPageOffset(array $params): int
  5478. {
  5479. $offset = 0;
  5480. $pageSize = $this->getPageSize($params);
  5481. if (isset($params['page'])) {
  5482. foreach ($params['page'] as $page) {
  5483. $parts = explode(',', $page, 2);
  5484. $page = intval($parts[0]) - 1;
  5485. $offset = $page * $pageSize;
  5486. }
  5487. }
  5488. return $offset;
  5489. }
  5490. private function getPageSize(array $params): int
  5491. {
  5492. $pageSize = $this->DEFAULT_PAGE_SIZE;
  5493. if (isset($params['page'])) {
  5494. foreach ($params['page'] as $page) {
  5495. $parts = explode(',', $page, 2);
  5496. if (count($parts) > 1) {
  5497. $pageSize = intval($parts[1]);
  5498. }
  5499. }
  5500. }
  5501. return $pageSize;
  5502. }
  5503. public function getResultSize(array $params): int
  5504. {
  5505. $numberOfRows = -1;
  5506. if (isset($params['size'])) {
  5507. foreach ($params['size'] as $size) {
  5508. $numberOfRows = intval($size);
  5509. }
  5510. }
  5511. return $numberOfRows;
  5512. }
  5513. public function getPageLimit(array $params): int
  5514. {
  5515. $pageLimit = -1;
  5516. if ($this->hasPage($params)) {
  5517. $pageLimit = $this->getPageSize($params);
  5518. }
  5519. $resultSize = $this->getResultSize($params);
  5520. if ($resultSize >= 0) {
  5521. if ($pageLimit >= 0) {
  5522. $pageLimit = min($pageLimit, $resultSize);
  5523. } else {
  5524. $pageLimit = $resultSize;
  5525. }
  5526. }
  5527. return $pageLimit;
  5528. }
  5529. }
  5530. // file: src/Tqdev/PhpCrudApi/Record/PathTree.php
  5531. class PathTree implements \JsonSerializable
  5532. {
  5533. const WILDCARD = '*';
  5534. private $tree;
  5535. public function __construct( /* object */&$tree = null)
  5536. {
  5537. if (!$tree) {
  5538. $tree = $this->newTree();
  5539. }
  5540. $this->tree = &$tree;
  5541. }
  5542. public function newTree()
  5543. {
  5544. return (object) ['values' => [], 'branches' => (object) []];
  5545. }
  5546. public function getKeys(): array
  5547. {
  5548. $branches = (array) $this->tree->branches;
  5549. return array_keys($branches);
  5550. }
  5551. public function getValues(): array
  5552. {
  5553. return $this->tree->values;
  5554. }
  5555. public function get(string $key): PathTree
  5556. {
  5557. if (!isset($this->tree->branches->$key)) {
  5558. return null;
  5559. }
  5560. return new PathTree($this->tree->branches->$key);
  5561. }
  5562. public function put(array $path, $value)
  5563. {
  5564. $tree = &$this->tree;
  5565. foreach ($path as $key) {
  5566. if (!isset($tree->branches->$key)) {
  5567. $tree->branches->$key = $this->newTree();
  5568. }
  5569. $tree = &$tree->branches->$key;
  5570. }
  5571. $tree->values[] = $value;
  5572. }
  5573. public function match(array $path): array
  5574. {
  5575. $star = self::WILDCARD;
  5576. $tree = &$this->tree;
  5577. foreach ($path as $key) {
  5578. if (isset($tree->branches->$key)) {
  5579. $tree = &$tree->branches->$key;
  5580. } else if (isset($tree->branches->$star)) {
  5581. $tree = &$tree->branches->$star;
  5582. } else {
  5583. return [];
  5584. }
  5585. }
  5586. return $tree->values;
  5587. }
  5588. public static function fromJson( /* object */$tree): PathTree
  5589. {
  5590. return new PathTree($tree);
  5591. }
  5592. public function jsonSerialize()
  5593. {
  5594. return $this->tree;
  5595. }
  5596. }
  5597. // file: src/Tqdev/PhpCrudApi/Record/RecordService.php
  5598. class RecordService
  5599. {
  5600. private $db;
  5601. private $reflection;
  5602. private $columns;
  5603. private $joiner;
  5604. private $filters;
  5605. private $ordering;
  5606. private $pagination;
  5607. public function __construct(GenericDB $db, ReflectionService $reflection)
  5608. {
  5609. $this->db = $db;
  5610. $this->reflection = $reflection;
  5611. $this->columns = new ColumnIncluder();
  5612. $this->joiner = new RelationJoiner($reflection, $this->columns);
  5613. $this->filters = new FilterInfo();
  5614. $this->ordering = new OrderingInfo();
  5615. $this->pagination = new PaginationInfo();
  5616. }
  5617. private function sanitizeRecord(string $tableName, /* object */ $record, string $id)
  5618. {
  5619. $keyset = array_keys((array) $record);
  5620. foreach ($keyset as $key) {
  5621. if (!$this->reflection->getTable($tableName)->hasColumn($key)) {
  5622. unset($record->$key);
  5623. }
  5624. }
  5625. if ($id != '') {
  5626. $pk = $this->reflection->getTable($tableName)->getPk();
  5627. foreach ($this->reflection->getTable($tableName)->getColumnNames() as $key) {
  5628. $field = $this->reflection->getTable($tableName)->getColumn($key);
  5629. if ($field->getName() == $pk->getName()) {
  5630. unset($record->$key);
  5631. }
  5632. }
  5633. }
  5634. }
  5635. public function hasTable(string $table): bool
  5636. {
  5637. return $this->reflection->hasTable($table);
  5638. }
  5639. public function getType(string $table): string
  5640. {
  5641. return $this->reflection->getType($table);
  5642. }
  5643. public function create(string $tableName, /* object */ $record, array $params)
  5644. {
  5645. $this->sanitizeRecord($tableName, $record, '');
  5646. $table = $this->reflection->getTable($tableName);
  5647. $columnValues = $this->columns->getValues($table, true, $record, $params);
  5648. return $this->db->createSingle($table, $columnValues);
  5649. }
  5650. public function read(string $tableName, string $id, array $params) /*: ?object*/
  5651. {
  5652. $table = $this->reflection->getTable($tableName);
  5653. $this->joiner->addMandatoryColumns($table, $params);
  5654. $columnNames = $this->columns->getNames($table, true, $params);
  5655. $record = $this->db->selectSingle($table, $columnNames, $id);
  5656. if ($record == null) {
  5657. return null;
  5658. }
  5659. $records = array($record);
  5660. $this->joiner->addJoins($table, $records, $params, $this->db);
  5661. return $records[0];
  5662. }
  5663. public function update(string $tableName, string $id, /* object */ $record, array $params)
  5664. {
  5665. $this->sanitizeRecord($tableName, $record, $id);
  5666. $table = $this->reflection->getTable($tableName);
  5667. $columnValues = $this->columns->getValues($table, true, $record, $params);
  5668. return $this->db->updateSingle($table, $columnValues, $id);
  5669. }
  5670. public function delete(string $tableName, string $id, array $params)
  5671. {
  5672. $table = $this->reflection->getTable($tableName);
  5673. return $this->db->deleteSingle($table, $id);
  5674. }
  5675. public function increment(string $tableName, string $id, /* object */ $record, array $params)
  5676. {
  5677. $this->sanitizeRecord($tableName, $record, $id);
  5678. $table = $this->reflection->getTable($tableName);
  5679. $columnValues = $this->columns->getValues($table, true, $record, $params);
  5680. return $this->db->incrementSingle($table, $columnValues, $id);
  5681. }
  5682. public function _list(string $tableName, array $params): ListDocument
  5683. {
  5684. $table = $this->reflection->getTable($tableName);
  5685. $this->joiner->addMandatoryColumns($table, $params);
  5686. $columnNames = $this->columns->getNames($table, true, $params);
  5687. $condition = $this->filters->getCombinedConditions($table, $params);
  5688. $columnOrdering = $this->ordering->getColumnOrdering($table, $params);
  5689. if (!$this->pagination->hasPage($params)) {
  5690. $offset = 0;
  5691. $limit = $this->pagination->getPageLimit($params);
  5692. $count = 0;
  5693. } else {
  5694. $offset = $this->pagination->getPageOffset($params);
  5695. $limit = $this->pagination->getPageLimit($params);
  5696. $count = $this->db->selectCount($table, $condition);
  5697. }
  5698. $records = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, $offset, $limit);
  5699. $this->joiner->addJoins($table, $records, $params, $this->db);
  5700. return new ListDocument($records, $count);
  5701. }
  5702. }
  5703. // file: src/Tqdev/PhpCrudApi/Record/RelationJoiner.php
  5704. class RelationJoiner
  5705. {
  5706. private $reflection;
  5707. private $columns;
  5708. public function __construct(ReflectionService $reflection, ColumnIncluder $columns)
  5709. {
  5710. $this->reflection = $reflection;
  5711. $this->ordering = new OrderingInfo();
  5712. $this->columns = $columns;
  5713. }
  5714. public function addMandatoryColumns(ReflectedTable $table, array &$params) /*: void*/
  5715. {
  5716. if (!isset($params['join']) || !isset($params['include'])) {
  5717. return;
  5718. }
  5719. $params['mandatory'] = array();
  5720. foreach ($params['join'] as $tableNames) {
  5721. $t1 = $table;
  5722. foreach (explode(',', $tableNames) as $tableName) {
  5723. if (!$this->reflection->hasTable($tableName)) {
  5724. continue;
  5725. }
  5726. $t2 = $this->reflection->getTable($tableName);
  5727. $fks1 = $t1->getFksTo($t2->getName());
  5728. $t3 = $this->hasAndBelongsToMany($t1, $t2);
  5729. if ($t3 != null || count($fks1) > 0) {
  5730. $params['mandatory'][] = $t2->getName() . '.' . $t2->getPk()->getName();
  5731. }
  5732. foreach ($fks1 as $fk) {
  5733. $params['mandatory'][] = $t1->getName() . '.' . $fk->getName();
  5734. }
  5735. $fks2 = $t2->getFksTo($t1->getName());
  5736. if ($t3 != null || count($fks2) > 0) {
  5737. $params['mandatory'][] = $t1->getName() . '.' . $t1->getPk()->getName();
  5738. }
  5739. foreach ($fks2 as $fk) {
  5740. $params['mandatory'][] = $t2->getName() . '.' . $fk->getName();
  5741. }
  5742. $t1 = $t2;
  5743. }
  5744. }
  5745. }
  5746. private function getJoinsAsPathTree(array $params): PathTree
  5747. {
  5748. $joins = new PathTree();
  5749. if (isset($params['join'])) {
  5750. foreach ($params['join'] as $tableNames) {
  5751. $path = array();
  5752. foreach (explode(',', $tableNames) as $tableName) {
  5753. $t = $this->reflection->getTable($tableName);
  5754. if ($t != null) {
  5755. $path[] = $t->getName();
  5756. }
  5757. }
  5758. $joins->put($path, true);
  5759. }
  5760. }
  5761. return $joins;
  5762. }
  5763. public function addJoins(ReflectedTable $table, array &$records, array $params, GenericDB $db) /*: void*/
  5764. {
  5765. $joins = $this->getJoinsAsPathTree($params);
  5766. $this->addJoinsForTables($table, $joins, $records, $params, $db);
  5767. }
  5768. private function hasAndBelongsToMany(ReflectedTable $t1, ReflectedTable $t2) /*: ?ReflectedTable*/
  5769. {
  5770. foreach ($this->reflection->getTableNames() as $tableName) {
  5771. $t3 = $this->reflection->getTable($tableName);
  5772. if (count($t3->getFksTo($t1->getName())) > 0 && count($t3->getFksTo($t2->getName())) > 0) {
  5773. return $t3;
  5774. }
  5775. }
  5776. return null;
  5777. }
  5778. private function addJoinsForTables(ReflectedTable $t1, PathTree $joins, array &$records, array $params, GenericDB $db)
  5779. {
  5780. foreach ($joins->getKeys() as $t2Name) {
  5781. $t2 = $this->reflection->getTable($t2Name);
  5782. $belongsTo = count($t1->getFksTo($t2->getName())) > 0;
  5783. $hasMany = count($t2->getFksTo($t1->getName())) > 0;
  5784. if (!$belongsTo && !$hasMany) {
  5785. $t3 = $this->hasAndBelongsToMany($t1, $t2);
  5786. } else {
  5787. $t3 = null;
  5788. }
  5789. $hasAndBelongsToMany = ($t3 != null);
  5790. $newRecords = array();
  5791. $fkValues = null;
  5792. $pkValues = null;
  5793. $habtmValues = null;
  5794. if ($belongsTo) {
  5795. $fkValues = $this->getFkEmptyValues($t1, $t2, $records);
  5796. $this->addFkRecords($t2, $fkValues, $params, $db, $newRecords);
  5797. }
  5798. if ($hasMany) {
  5799. $pkValues = $this->getPkEmptyValues($t1, $records);
  5800. $this->addPkRecords($t1, $t2, $pkValues, $params, $db, $newRecords);
  5801. }
  5802. if ($hasAndBelongsToMany) {
  5803. $habtmValues = $this->getHabtmEmptyValues($t1, $t2, $t3, $db, $records);
  5804. $this->addFkRecords($t2, $habtmValues->fkValues, $params, $db, $newRecords);
  5805. }
  5806. $this->addJoinsForTables($t2, $joins->get($t2Name), $newRecords, $params, $db);
  5807. if ($fkValues != null) {
  5808. $this->fillFkValues($t2, $newRecords, $fkValues);
  5809. $this->setFkValues($t1, $t2, $records, $fkValues);
  5810. }
  5811. if ($pkValues != null) {
  5812. $this->fillPkValues($t1, $t2, $newRecords, $pkValues);
  5813. $this->setPkValues($t1, $t2, $records, $pkValues);
  5814. }
  5815. if ($habtmValues != null) {
  5816. $this->fillFkValues($t2, $newRecords, $habtmValues->fkValues);
  5817. $this->setHabtmValues($t1, $t2, $records, $habtmValues);
  5818. }
  5819. }
  5820. }
  5821. private function getFkEmptyValues(ReflectedTable $t1, ReflectedTable $t2, array $records): array
  5822. {
  5823. $fkValues = array();
  5824. $fks = $t1->getFksTo($t2->getName());
  5825. foreach ($fks as $fk) {
  5826. $fkName = $fk->getName();
  5827. foreach ($records as $record) {
  5828. if (isset($record[$fkName])) {
  5829. $fkValue = $record[$fkName];
  5830. $fkValues[$fkValue] = null;
  5831. }
  5832. }
  5833. }
  5834. return $fkValues;
  5835. }
  5836. private function addFkRecords(ReflectedTable $t2, array $fkValues, array $params, GenericDB $db, array &$records) /*: void*/
  5837. {
  5838. $columnNames = $this->columns->getNames($t2, false, $params);
  5839. $fkIds = array_keys($fkValues);
  5840. foreach ($db->selectMultiple($t2, $columnNames, $fkIds) as $record) {
  5841. $records[] = $record;
  5842. }
  5843. }
  5844. private function fillFkValues(ReflectedTable $t2, array $fkRecords, array &$fkValues) /*: void*/
  5845. {
  5846. $pkName = $t2->getPk()->getName();
  5847. foreach ($fkRecords as $fkRecord) {
  5848. $pkValue = $fkRecord[$pkName];
  5849. $fkValues[$pkValue] = $fkRecord;
  5850. }
  5851. }
  5852. private function setFkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $fkValues) /*: void*/
  5853. {
  5854. $fks = $t1->getFksTo($t2->getName());
  5855. foreach ($fks as $fk) {
  5856. $fkName = $fk->getName();
  5857. foreach ($records as $i => $record) {
  5858. if (isset($record[$fkName])) {
  5859. $key = $record[$fkName];
  5860. $records[$i][$fkName] = $fkValues[$key];
  5861. }
  5862. }
  5863. }
  5864. }
  5865. private function getPkEmptyValues(ReflectedTable $t1, array $records): array
  5866. {
  5867. $pkValues = array();
  5868. $pkName = $t1->getPk()->getName();
  5869. foreach ($records as $record) {
  5870. $key = $record[$pkName];
  5871. $pkValues[$key] = array();
  5872. }
  5873. return $pkValues;
  5874. }
  5875. private function addPkRecords(ReflectedTable $t1, ReflectedTable $t2, array $pkValues, array $params, GenericDB $db, array &$records) /*: void*/
  5876. {
  5877. $fks = $t2->getFksTo($t1->getName());
  5878. $columnNames = $this->columns->getNames($t2, false, $params);
  5879. $pkValueKeys = implode(',', array_keys($pkValues));
  5880. $conditions = array();
  5881. foreach ($fks as $fk) {
  5882. $conditions[] = new ColumnCondition($fk, 'in', $pkValueKeys);
  5883. }
  5884. $condition = OrCondition::fromArray($conditions);
  5885. $columnOrdering = array();
  5886. $limit = VariableStore::get("joinLimits.maxRecords") ?: -1;
  5887. if ($limit != -1) {
  5888. $columnOrdering = $this->ordering->getDefaultColumnOrdering($t2);
  5889. }
  5890. foreach ($db->selectAll($t2, $columnNames, $condition, $columnOrdering, 0, $limit) as $record) {
  5891. $records[] = $record;
  5892. }
  5893. }
  5894. private function fillPkValues(ReflectedTable $t1, ReflectedTable $t2, array $pkRecords, array &$pkValues) /*: void*/
  5895. {
  5896. $fks = $t2->getFksTo($t1->getName());
  5897. foreach ($fks as $fk) {
  5898. $fkName = $fk->getName();
  5899. foreach ($pkRecords as $pkRecord) {
  5900. $key = $pkRecord[$fkName];
  5901. if (isset($pkValues[$key])) {
  5902. $pkValues[$key][] = $pkRecord;
  5903. }
  5904. }
  5905. }
  5906. }
  5907. private function setPkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $pkValues) /*: void*/
  5908. {
  5909. $pkName = $t1->getPk()->getName();
  5910. $t2Name = $t2->getName();
  5911. foreach ($records as $i => $record) {
  5912. $key = $record[$pkName];
  5913. $records[$i][$t2Name] = $pkValues[$key];
  5914. }
  5915. }
  5916. private function getHabtmEmptyValues(ReflectedTable $t1, ReflectedTable $t2, ReflectedTable $t3, GenericDB $db, array $records): HabtmValues
  5917. {
  5918. $pkValues = $this->getPkEmptyValues($t1, $records);
  5919. $fkValues = array();
  5920. $fk1 = $t3->getFksTo($t1->getName())[0];
  5921. $fk2 = $t3->getFksTo($t2->getName())[0];
  5922. $fk1Name = $fk1->getName();
  5923. $fk2Name = $fk2->getName();
  5924. $columnNames = array($fk1Name, $fk2Name);
  5925. $pkIds = implode(',', array_keys($pkValues));
  5926. $condition = new ColumnCondition($t3->getColumn($fk1Name), 'in', $pkIds);
  5927. $columnOrdering = array();
  5928. $limit = VariableStore::get("joinLimits.maxRecords") ?: -1;
  5929. if ($limit != -1) {
  5930. $columnOrdering = $this->ordering->getDefaultColumnOrdering($t3);
  5931. }
  5932. $records = $db->selectAll($t3, $columnNames, $condition, $columnOrdering, 0, $limit);
  5933. foreach ($records as $record) {
  5934. $val1 = $record[$fk1Name];
  5935. $val2 = $record[$fk2Name];
  5936. $pkValues[$val1][] = $val2;
  5937. $fkValues[$val2] = null;
  5938. }
  5939. return new HabtmValues($pkValues, $fkValues);
  5940. }
  5941. private function setHabtmValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, HabtmValues $habtmValues) /*: void*/
  5942. {
  5943. $pkName = $t1->getPk()->getName();
  5944. $t2Name = $t2->getName();
  5945. foreach ($records as $i => $record) {
  5946. $key = $record[$pkName];
  5947. $val = array();
  5948. $fks = $habtmValues->pkValues[$key];
  5949. foreach ($fks as $fk) {
  5950. $val[] = $habtmValues->fkValues[$fk];
  5951. }
  5952. $records[$i][$t2Name] = $val;
  5953. }
  5954. }
  5955. }
  5956. // file: src/Tqdev/PhpCrudApi/Api.php
  5957. class Api
  5958. {
  5959. private $router;
  5960. private $responder;
  5961. private $debug;
  5962. public function __construct(Config $config)
  5963. {
  5964. $db = new GenericDB(
  5965. $config->getDriver(),
  5966. $config->getAddress(),
  5967. $config->getPort(),
  5968. $config->getDatabase(),
  5969. $config->getUsername(),
  5970. $config->getPassword()
  5971. );
  5972. $cache = CacheFactory::create($config);
  5973. $reflection = new ReflectionService($db, $cache, $config->getCacheTime());
  5974. $responder = new Responder();
  5975. $router = new SimpleRouter($responder, $cache, $config->getCacheTime(), $config->getDebug());
  5976. foreach ($config->getMiddlewares() as $middleware => $properties) {
  5977. switch ($middleware) {
  5978. case 'cors':
  5979. new CorsMiddleware($router, $responder, $properties);
  5980. break;
  5981. case 'firewall':
  5982. new FirewallMiddleware($router, $responder, $properties);
  5983. break;
  5984. case 'basicAuth':
  5985. new BasicAuthMiddleware($router, $responder, $properties);
  5986. break;
  5987. case 'jwtAuth':
  5988. new JwtAuthMiddleware($router, $responder, $properties);
  5989. break;
  5990. case 'validation':
  5991. new ValidationMiddleware($router, $responder, $properties, $reflection);
  5992. break;
  5993. case 'ipAddress':
  5994. new IpAddressMiddleware($router, $responder, $properties, $reflection);
  5995. break;
  5996. case 'sanitation':
  5997. new SanitationMiddleware($router, $responder, $properties, $reflection);
  5998. break;
  5999. case 'multiTenancy':
  6000. new MultiTenancyMiddleware($router, $responder, $properties, $reflection);
  6001. break;
  6002. case 'authorization':
  6003. new AuthorizationMiddleware($router, $responder, $properties, $reflection);
  6004. break;
  6005. case 'xsrf':
  6006. new XsrfMiddleware($router, $responder, $properties);
  6007. break;
  6008. case 'pageLimits':
  6009. new PageLimitsMiddleware($router, $responder, $properties, $reflection);
  6010. break;
  6011. case 'joinLimits':
  6012. new JoinLimitsMiddleware($router, $responder, $properties, $reflection);
  6013. break;
  6014. case 'customization':
  6015. new CustomizationMiddleware($router, $responder, $properties, $reflection);
  6016. break;
  6017. }
  6018. }
  6019. foreach ($config->getControllers() as $controller) {
  6020. switch ($controller) {
  6021. case 'records':
  6022. $records = new RecordService($db, $reflection);
  6023. new RecordController($router, $responder, $records);
  6024. break;
  6025. case 'columns':
  6026. $definition = new DefinitionService($db, $reflection);
  6027. new ColumnController($router, $responder, $reflection, $definition);
  6028. break;
  6029. case 'cache':
  6030. new CacheController($router, $responder, $cache);
  6031. break;
  6032. case 'openapi':
  6033. $openApi = new OpenApiService($reflection, $config->getOpenApiBase());
  6034. new OpenApiController($router, $responder, $openApi);
  6035. break;
  6036. }
  6037. }
  6038. $this->router = $router;
  6039. $this->responder = $responder;
  6040. $this->debug = $config->getDebug();
  6041. }
  6042. public function handle(ServerRequestInterface $request): ResponseInterface
  6043. {
  6044. $response = null;
  6045. try {
  6046. $response = $this->router->route($request);
  6047. } catch (\Throwable $e) {
  6048. $response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
  6049. if ($this->debug) {
  6050. $response = ResponseUtils::addExceptionHeaders($response, $e);
  6051. }
  6052. }
  6053. return $response;
  6054. }
  6055. }
  6056. // file: src/Tqdev/PhpCrudApi/Config.php
  6057. class Config
  6058. {
  6059. private $values = [
  6060. 'driver' => null,
  6061. 'address' => 'localhost',
  6062. 'port' => null,
  6063. 'username' => null,
  6064. 'password' => null,
  6065. 'database' => null,
  6066. 'middlewares' => 'cors',
  6067. 'controllers' => 'records,openapi',
  6068. 'cacheType' => 'TempFile',
  6069. 'cachePath' => '',
  6070. 'cacheTime' => 10,
  6071. 'debug' => false,
  6072. 'openApiBase' => '{"info":{"title":"PHP-CRUD-API","version":"1.0.0"}}',
  6073. ];
  6074. private function getDefaultDriver(array $values): string
  6075. {
  6076. if (isset($values['driver'])) {
  6077. return $values['driver'];
  6078. }
  6079. return 'mysql';
  6080. }
  6081. private function getDefaultPort(string $driver): int
  6082. {
  6083. switch ($driver) {
  6084. case 'mysql':return 3306;
  6085. case 'pgsql':return 5432;
  6086. case 'sqlsrv':return 1433;
  6087. }
  6088. }
  6089. private function getDefaultAddress(string $driver): string
  6090. {
  6091. switch ($driver) {
  6092. case 'mysql':return 'localhost';
  6093. case 'pgsql':return 'localhost';
  6094. case 'sqlsrv':return 'localhost';
  6095. }
  6096. }
  6097. private function getDriverDefaults(string $driver): array
  6098. {
  6099. return [
  6100. 'driver' => $driver,
  6101. 'address' => $this->getDefaultAddress($driver),
  6102. 'port' => $this->getDefaultPort($driver),
  6103. ];
  6104. }
  6105. public function __construct(array $values)
  6106. {
  6107. $driver = $this->getDefaultDriver($values);
  6108. $defaults = $this->getDriverDefaults($driver);
  6109. $newValues = array_merge($this->values, $defaults, $values);
  6110. $newValues = $this->parseMiddlewares($newValues);
  6111. $diff = array_diff_key($newValues, $this->values);
  6112. if (!empty($diff)) {
  6113. $key = array_keys($diff)[0];
  6114. throw new \Exception("Config has invalid value '$key'");
  6115. }
  6116. $this->values = $newValues;
  6117. }
  6118. private function parseMiddlewares(array $values): array
  6119. {
  6120. $newValues = array();
  6121. $properties = array();
  6122. $middlewares = array_map('trim', explode(',', $values['middlewares']));
  6123. foreach ($middlewares as $middleware) {
  6124. $properties[$middleware] = [];
  6125. }
  6126. foreach ($values as $key => $value) {
  6127. if (strpos($key, '.') === false) {
  6128. $newValues[$key] = $value;
  6129. } else {
  6130. list($middleware, $key2) = explode('.', $key, 2);
  6131. if (isset($properties[$middleware])) {
  6132. $properties[$middleware][$key2] = $value;
  6133. } else {
  6134. throw new \Exception("Config has invalid value '$key'");
  6135. }
  6136. }
  6137. }
  6138. $newValues['middlewares'] = array_reverse($properties, true);
  6139. return $newValues;
  6140. }
  6141. public function getDriver(): string
  6142. {
  6143. return $this->values['driver'];
  6144. }
  6145. public function getAddress(): string
  6146. {
  6147. return $this->values['address'];
  6148. }
  6149. public function getPort(): int
  6150. {
  6151. return $this->values['port'];
  6152. }
  6153. public function getUsername(): string
  6154. {
  6155. return $this->values['username'];
  6156. }
  6157. public function getPassword(): string
  6158. {
  6159. return $this->values['password'];
  6160. }
  6161. public function getDatabase(): string
  6162. {
  6163. return $this->values['database'];
  6164. }
  6165. public function getMiddlewares(): array
  6166. {
  6167. return $this->values['middlewares'];
  6168. }
  6169. public function getControllers(): array
  6170. {
  6171. return array_map('trim', explode(',', $this->values['controllers']));
  6172. }
  6173. public function getCacheType(): string
  6174. {
  6175. return $this->values['cacheType'];
  6176. }
  6177. public function getCachePath(): string
  6178. {
  6179. return $this->values['cachePath'];
  6180. }
  6181. public function getCacheTime(): int
  6182. {
  6183. return $this->values['cacheTime'];
  6184. }
  6185. public function getDebug(): string
  6186. {
  6187. return $this->values['debug'];
  6188. }
  6189. public function getOpenApiBase(): array
  6190. {
  6191. return json_decode($this->values['openApiBase'], true);
  6192. }
  6193. }
  6194. // file: src/Tqdev/PhpCrudApi/RequestFactory.php
  6195. class RequestFactory
  6196. {
  6197. private static function parseBody(string $body) /*: ?object*/
  6198. {
  6199. $first = substr($body, 0, 1);
  6200. if ($first == '[' || $first == '{') {
  6201. $object = json_decode($body);
  6202. $causeCode = json_last_error();
  6203. if ($causeCode !== JSON_ERROR_NONE) {
  6204. $object = null;
  6205. }
  6206. } else {
  6207. parse_str($body, $input);
  6208. foreach ($input as $key => $value) {
  6209. if (substr($key, -9) == '__is_null') {
  6210. $input[substr($key, 0, -9)] = null;
  6211. unset($input[$key]);
  6212. }
  6213. }
  6214. $object = (object) $input;
  6215. }
  6216. return $object;
  6217. }
  6218. public static function fromGlobals(): ServerRequestInterface
  6219. {
  6220. $psr17Factory = new Psr17Factory();
  6221. $creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
  6222. $serverRequest = $creator->fromGlobals();
  6223. $uri = '';
  6224. if (isset($_SERVER['PATH_INFO'])) {
  6225. $uri .= $_SERVER['PATH_INFO'];
  6226. }
  6227. if (isset($_SERVER['QUERY_STRING'])) {
  6228. $uri .= '?' . $_SERVER['QUERY_STRING'];
  6229. }
  6230. if ($uri) {
  6231. $serverRequest = $serverRequest->withUri($psr17Factory->createUri($uri));
  6232. }
  6233. $body = file_get_contents('php://input');
  6234. if ($body) {
  6235. $serverRequest = $serverRequest->withParsedBody(self::parseBody($body));
  6236. }
  6237. return $serverRequest;
  6238. }
  6239. public static function fromString(string $request): ServerRequestInterface
  6240. {
  6241. $parts = explode("\n\n", trim($request), 2);
  6242. $lines = explode("\n", $parts[0]);
  6243. $first = explode(' ', trim(array_shift($lines)), 2);
  6244. $method = $first[0];
  6245. $body = isset($parts[1]) ? $parts[1] : '';
  6246. $url = isset($first[1]) ? $first[1] : '';
  6247. $psr17Factory = new Psr17Factory();
  6248. $serverRequest = $psr17Factory->createServerRequest($method, $url);
  6249. foreach ($lines as $line) {
  6250. list($key, $value) = explode(':', $line, 2);
  6251. $serverRequest = $serverRequest->withAddedHeader($key, $value);
  6252. }
  6253. if ($body) {
  6254. $stream = $psr17Factory->createStream($body);
  6255. $stream->rewind();
  6256. $serverRequest = $serverRequest->withBody($stream);
  6257. $serverRequest = $serverRequest->withParsedBody(self::parseBody($body));
  6258. }
  6259. return $serverRequest;
  6260. }
  6261. }
  6262. // file: src/Tqdev/PhpCrudApi/RequestUtils.php
  6263. class RequestUtils
  6264. {
  6265. public static function setParams(ServerRequestInterface $request, array $params): ServerRequestInterface
  6266. {
  6267. $query = preg_replace('|%5B[0-9]+%5D=|', '=', http_build_query($params));
  6268. return $request->withUri($request->getUri()->withQuery($query));
  6269. }
  6270. public static function getHeader(ServerRequestInterface $request, string $header): string
  6271. {
  6272. $headers = $request->getHeader($header);
  6273. return isset($headers[0]) ? $headers[0] : '';
  6274. }
  6275. public static function getParams(ServerRequestInterface $request): array
  6276. {
  6277. $params = array();
  6278. $query = $request->getUri()->getQuery();
  6279. $query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
  6280. parse_str($query, $params);
  6281. return $params;
  6282. }
  6283. public static function getPathSegment(ServerRequestInterface $request, int $part): string
  6284. {
  6285. $pathSegments = explode('/', rtrim($request->getUri()->getPath(), '/'));
  6286. if ($part < 0 || $part >= count($pathSegments)) {
  6287. return '';
  6288. }
  6289. return urldecode($pathSegments[$part]);
  6290. }
  6291. public static function getOperation(ServerRequestInterface $request): string
  6292. {
  6293. $method = $request->getMethod();
  6294. $path = RequestUtils::getPathSegment($request, 1);
  6295. $hasPk = RequestUtils::getPathSegment($request, 3) != '';
  6296. switch ($path) {
  6297. case 'openapi':
  6298. return 'document';
  6299. case 'columns':
  6300. return $method == 'get' ? 'reflect' : 'remodel';
  6301. case 'records':
  6302. switch ($method) {
  6303. case 'POST':
  6304. return 'create';
  6305. case 'GET':
  6306. return $hasPk ? 'read' : 'list';
  6307. case 'PUT':
  6308. return 'update';
  6309. case 'DELETE':
  6310. return 'delete';
  6311. case 'PATCH':
  6312. return 'increment';
  6313. }
  6314. }
  6315. return 'unknown';
  6316. }
  6317. private static function getJoinTables(string $tableName, array $parameters): array
  6318. {
  6319. $uniqueTableNames = array();
  6320. $uniqueTableNames[$tableName] = true;
  6321. if (isset($parameters['join'])) {
  6322. foreach ($parameters['join'] as $parameter) {
  6323. $tableNames = explode(',', trim($parameter));
  6324. foreach ($tableNames as $tableName) {
  6325. $uniqueTableNames[$tableName] = true;
  6326. }
  6327. }
  6328. }
  6329. return array_keys($uniqueTableNames);
  6330. }
  6331. public static function getTableNames(ServerRequestInterface $request, ReflectionService $reflection): array
  6332. {
  6333. $path = RequestUtils::getPathSegment($request, 1);
  6334. $tableName = RequestUtils::getPathSegment($request, 2);
  6335. $allTableNames = $reflection->getTableNames();
  6336. switch ($path) {
  6337. case 'openapi':
  6338. return $allTableNames;
  6339. case 'columns':
  6340. return $tableName ? [$tableName] : $allTableNames;
  6341. case 'records':
  6342. return self::getJoinTables($tableName, RequestUtils::getParams($request));
  6343. }
  6344. return $allTableNames;
  6345. }
  6346. }
  6347. // file: src/Tqdev/PhpCrudApi/ResponseFactory.php
  6348. class ResponseFactory
  6349. {
  6350. const OK = 200;
  6351. const UNAUTHORIZED = 401;
  6352. const FORBIDDEN = 403;
  6353. const NOT_FOUND = 404;
  6354. const METHOD_NOT_ALLOWED = 405;
  6355. const CONFLICT = 409;
  6356. const UNPROCESSABLE_ENTITY = 422;
  6357. const INTERNAL_SERVER_ERROR = 500;
  6358. public static function fromObject(int $status, $body): ResponseInterface
  6359. {
  6360. $psr17Factory = new Psr17Factory();
  6361. $response = $psr17Factory->createResponse($status);
  6362. $content = json_encode($body, JSON_UNESCAPED_UNICODE);
  6363. $stream = $psr17Factory->createStream($content);
  6364. $stream->rewind();
  6365. $response = $response->withBody($stream);
  6366. $response = $response->withHeader('Content-Type', 'application/json');
  6367. $response = $response->withHeader('Content-Length', strlen($content));
  6368. return $response;
  6369. }
  6370. public static function fromStatus(int $status): ResponseInterface
  6371. {
  6372. $psr17Factory = new Psr17Factory();
  6373. return $psr17Factory->createResponse($status);
  6374. }
  6375. }
  6376. // file: src/Tqdev/PhpCrudApi/ResponseUtils.php
  6377. class ResponseUtils
  6378. {
  6379. public static function output(ResponseInterface $response)
  6380. {
  6381. $status = $response->getStatusCode();
  6382. $headers = $response->getHeaders();
  6383. $body = $response->getBody()->getContents();
  6384. http_response_code($status);
  6385. foreach ($headers as $key => $values) {
  6386. foreach ($values as $value) {
  6387. header("$key: $value");
  6388. }
  6389. }
  6390. echo $body;
  6391. }
  6392. public static function addExceptionHeaders(ResponseInterface $response, \Throwable $e): ResponseInterface
  6393. {
  6394. $response = $response->withHeader('X-Exception-Name', get_class($e));
  6395. $response = $response->withHeader('X-Exception-Message', $e->getMessage());
  6396. $response = $response->withHeader('X-Exception-File', $e->getFile() . ':' . $e->getLine());
  6397. return $response;
  6398. }
  6399. public static function toString(ResponseInterface $response): string
  6400. {
  6401. $status = $response->getStatusCode();
  6402. $headers = $response->getHeaders();
  6403. $body = $response->getBody()->getContents();
  6404. $str = "$status\n";
  6405. foreach ($headers as $key => $values) {
  6406. foreach ($values as $value) {
  6407. $str .= "$key: $value\n";
  6408. }
  6409. }
  6410. if ($body !== '') {
  6411. $str .= "\n";
  6412. $str .= "$body\n";
  6413. }
  6414. return $str;
  6415. }
  6416. }
  6417. // file: src/index.php
  6418. $config = new Config([
  6419. 'username' => 'php-crud-api',
  6420. 'password' => 'php-crud-api',
  6421. 'database' => 'php-crud-api',
  6422. ]);
  6423. $request = RequestFactory::fromGlobals();
  6424. $api = new Api($config);
  6425. $response = $api->handle($request);
  6426. ResponseUtils::output($response);